@clawhub-alirezarezvani-9164a8924b
Cross-functional organizational health check combining signals from all C-suite roles. Scores 8 dimensions on a traffic-light scale with drill-down recommend...
---
name: "org-health-diagnostic"
description: "Cross-functional organizational health check combining signals from all C-suite roles. Scores 8 dimensions on a traffic-light scale with drill-down recommendations. Use when assessing overall company health, preparing for board reviews, identifying at-risk functions, or when user mentions org health, health check, or health dashboard."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: organizational-health
updated: 2026-03-05
python-tools: health_scorer.py
frameworks: health-benchmarks
---
# Org Health Diagnostic
Eight dimensions. Traffic lights. Real benchmarks. Surfaces the problems you don't know you have.
## Keywords
org health, organizational health, health diagnostic, health dashboard, health check, company health, functional health, team health, startup health, health scorecard, health assessment, risk dashboard
## Quick Start
```bash
python scripts/health_scorer.py # Guided CLI — enter metrics, get scored dashboard
python scripts/health_scorer.py --json # Output raw JSON for integration
```
Or describe your metrics:
```
/health [paste your key metrics or answer prompts]
/health:dimension [financial|revenue|product|engineering|people|ops|security|market]
```
## The 8 Dimensions
### 1. 💰 Financial Health (CFO)
**What it measures:** Can we fund operations and invest in growth?
Key metrics:
- **Runway** — months at current burn (Green: >12, Yellow: 6-12, Red: <6)
- **Burn multiple** — net burn / net new ARR (Green: <1.5x, Yellow: 1.5-2.5x, Red: >2.5x)
- **Gross margin** — SaaS target: >65% (Green: >70%, Yellow: 55-70%, Red: <55%)
- **MoM growth rate** — contextual by stage (see benchmarks)
- **Revenue concentration** — top customer % of ARR (Green: <15%, Yellow: 15-25%, Red: >25%)
### 2. 📈 Revenue Health (CRO)
**What it measures:** Are customers staying, growing, and recommending us?
Key metrics:
- **NRR (Net Revenue Retention)** — Green: >110%, Yellow: 100-110%, Red: <100%
- **Logo churn rate (annualized)** — Green: <5%, Yellow: 5-10%, Red: >10%
- **Pipeline coverage (next quarter)** — Green: >3x, Yellow: 2-3x, Red: <2x
- **CAC payback period** — Green: <12 months, Yellow: 12-18, Red: >18 months
- **Average ACV trend** — directional: growing, flat, declining
### 3. 🚀 Product Health (CPO)
**What it measures:** Do customers love and use the product?
Key metrics:
- **NPS** — Green: >40, Yellow: 20-40, Red: <20
- **DAU/MAU ratio** — engagement proxy (Green: >40%, Yellow: 20-40%, Red: <20%)
- **Core feature adoption** — % of users using primary value feature (Green: >60%)
- **Time-to-value** — days from signup to first core action (lower is better)
- **Customer satisfaction (CSAT)** — Green: >4.2/5, Yellow: 3.5-4.2, Red: <3.5
### 4. ⚙️ Engineering Health (CTO)
**What it measures:** Can we ship reliably and sustain velocity?
Key metrics:
- **Deployment frequency** — Green: daily, Yellow: weekly, Red: monthly or less
- **Change failure rate** — % of deployments causing incidents (Green: <5%, Red: >15%)
- **Mean time to recovery (MTTR)** — Green: <1 hour, Yellow: 1-4 hours, Red: >4 hours
- **Tech debt ratio** — % of sprint capacity on debt (Green: <20%, Yellow: 20-35%, Red: >35%)
- **Incident frequency** — P0/P1 per month (Green: <2, Yellow: 2-5, Red: >5)
### 5. 👥 People Health (CHRO)
**What it measures:** Is the team stable, engaged, and growing?
Key metrics:
- **Regrettable attrition (annualized)** — Green: <10%, Yellow: 10-20%, Red: >20%
- **Engagement score** — (eNPS or similar; Green: >30, Yellow: 0-30, Red: <0)
- **Time-to-fill (avg days)** — Green: <45, Yellow: 45-90, Red: >90
- **Manager-to-IC ratio** — Green: 1:5–1:8, Yellow: 1:3–1:5 or 1:8–1:12, Red: outside
- **Internal promotion rate** — at least 25-30% of senior roles filled internally
### 6. 🔄 Operational Health (COO)
**What it measures:** Are we executing our strategy with discipline?
Key metrics:
- **OKR completion rate** — % of key results hitting target (Green: >70%, Yellow: 50-70%, Red: <50%)
- **Decision cycle time** — days from decision needed to decision made (Green: <48h, Yellow: 48h-1w)
- **Meeting effectiveness** — % of meetings with clear outcome (qualitative)
- **Process maturity** — level 1-5 scale (see COO advisor)
- **Cross-functional initiative completion** — % on time, on scope
### 7. 🔒 Security Health (CISO)
**What it measures:** Are we protecting customers and maintaining compliance?
Key metrics:
- **Security incidents (last 90 days)** — Green: 0, Yellow: 1-2 minor, Red: 1+ major
- **Compliance status** — certifications current/in-progress vs. overdue
- **Vulnerability remediation SLA** — % of critical CVEs patched within SLA (Green: 100%)
- **Security training completion** — % of team current (Green: >95%)
- **Pen test recency** — Green: <12 months, Yellow: 12-24, Red: >24 months
### 8. 📣 Market Health (CMO)
**What it measures:** Are we winning in the market and growing efficiently?
Key metrics:
- **CAC trend** — improving, flat, or worsening QoQ
- **Organic vs paid lead mix** — more organic = healthier (less fragile)
- **Win rate** — % of qualified opportunities closed-won (Green: >25%, Yellow: 15-25%, Red: <15%)
- **Competitive win rate** — against primary competitors specifically
- **Brand NPS** — awareness + preference scores in ICP
---
## Scoring & Traffic Lights
Each dimension is scored 1-10 with traffic light:
- 🟢 **Green (7-10):** Healthy — maintain and optimize
- 🟡 **Yellow (4-6):** Watch — trend matters; improving or declining?
- 🔴 **Red (1-3):** Action required — address within 30 days
**Overall Health Score:**
Weighted average by company stage (see `references/health-benchmarks.md` for weights).
---
## Dimension Interactions (Why One Problem Creates Another)
| If this dimension is red... | Watch these dimensions next |
|-----------------------------|----------------------------|
| Financial Health | People (freeze hiring) → Engineering (freeze infra) → Product (cut scope) |
| Revenue Health | Financial (cash gap) → People (attrition risk) → Market (lose positioning) |
| People Health | Engineering (velocity drops) → Product (quality drops) → Revenue (churn rises) |
| Engineering Health | Product (features slip) → Revenue (deals stall on product) |
| Product Health | Revenue (NRR drops, churn rises) → Market (CAC rises; referrals dry up) |
| Operational Health | All dimensions degrade over time (execution failure cascades everywhere) |
---
## Dashboard Output Format
```
ORG HEALTH DIAGNOSTIC — [Company] — [Date]
Stage: [Seed/A/B/C] Overall: [Score]/10 Trend: [↑ Improving / → Stable / ↓ Declining]
DIMENSION SCORES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💰 Financial 🟢 8.2 Runway 14mo, burn 1.6x — strong
📈 Revenue 🟡 5.8 NRR 104%, pipeline thin (1.8x coverage)
🚀 Product 🟢 7.4 NPS 42, DAU/MAU 38%
⚙️ Engineering 🟡 5.2 Debt at 30%, MTTR 3.2h
👥 People 🔴 3.8 Attrition 24%, eng morale low
🔄 Operations 🟡 6.0 OKR 65% completion
🔒 Security 🟢 7.8 SOC 2 Type II complete, 0 incidents
📣 Market 🟡 5.5 CAC rising, win rate dropped to 22%
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TOP PRIORITIES
🔴 [1] People: attrition at 24% — engineering velocity will drop in 60 days
Action: CHRO + CEO to run retention audit; target top 5 at-risk this week
🟡 [2] Revenue: pipeline coverage at 1.8x — Q+1 miss risk is high
Action: CRO to add 3 qualified opps within 30 days or shift forecast down
🟡 [3] Engineering: tech debt at 30% of sprint — shipping will slow by Q3
Action: CTO to propose debt sprint plan; COO to protect capacity
WATCH
→ People → Engineering cascade risk if attrition continues (see dimension interactions)
```
---
## Graceful Degradation
You don't need all metrics to run a diagnostic. The tool handles partial data:
- Missing metric → excluded from score, flagged as "[data needed]"
- Score still valid for available dimensions
- Report flags which gaps to fill for next cycle
## References
- `references/health-benchmarks.md` — benchmarks by stage (Seed, A, B, C)
- `scripts/health_scorer.py` — CLI scoring tool with traffic light output
FILE:references/health-benchmarks.md
# Org Health Benchmarks by Stage
Benchmarks for scoring each dimension at Seed, Series A, Series B, and Series C.
---
## Financial Health Benchmarks (CFO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| Runway (green) | >18mo | >12mo | >12mo | >18mo |
| Runway (yellow) | 9-18mo | 6-12mo | 6-12mo | 9-18mo |
| Runway (red) | <9mo | <6mo | <6mo | <9mo |
| Burn multiple (green) | <3x | <2x | <1.5x | <1x |
| Burn multiple (yellow) | 3-5x | 2-3x | 1.5-2.5x | 1-1.5x |
| Gross margin (green) | >50% | >65% | >70% | >75% |
| MoM growth (green) | >15% | >10% | >7% | >5% |
| Revenue concentration | <30% | <25% | <15% | <10% |
**Stage-specific notes:**
- **Seed:** Burn multiple is looser — you're investing in PMF, not efficiency
- **Series A:** Efficiency starts to matter; board watching burn multiple closely
- **Series B:** Capital efficiency is table stakes; burn >2x raises serious questions
- **Series C:** Approaching path to profitability; investors expect <1.5x
---
## Revenue Health Benchmarks (CRO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| NRR (green) | >100% | >110% | >115% | >120% |
| NRR (yellow) | 90-100% | 100-110% | 105-115% | 110-120% |
| NRR (red) | <90% | <100% | <105% | <110% |
| Logo churn (green) | <15%/yr | <10%/yr | <7%/yr | <5%/yr |
| Pipeline coverage | >2x | >3x | >3.5x | >4x |
| CAC payback (green) | <24mo | <18mo | <12mo | <9mo |
| Win rate (green) | >20% | >25% | >28% | >30% |
| ACV trend | growing | growing | growing | growing |
**What "green" NRR signals:**
- >100%: product creates value; expansion outpaces churn
- >110%: customers grow inside your platform; land-and-expand working
- >120%: exceptional — net negative churn; growth from existing base alone
- <100%: customers leave faster than others expand; structural retention problem
**Warning: NRR can mask problems.** NRR of 110% with 25% logo churn means you're retaining revenue from large customers while losing small ones. Check both.
---
## Product Health Benchmarks (CPO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| NPS (green) | >30 | >40 | >45 | >50 |
| NPS (yellow) | 10-30 | 20-40 | 30-45 | 40-50 |
| NPS (red) | <10 | <20 | <30 | <40 |
| DAU/MAU (green) | >25% | >35% | >40% | >45% |
| Core feature adoption | >40% | >55% | >65% | >70% |
| Time-to-value | <7 days | <5 days | <3 days | <2 days |
| CSAT | >4.0/5 | >4.2/5 | >4.3/5 | >4.4/5 |
**PMF proxy metrics:**
- "Very disappointed" if product disappeared: >40% = strong PMF signal (Sean Ellis test)
- 6-month retention cohort: >40% is healthy; <20% means PMF not yet achieved
- Organic referral rate: >20% of new users from referrals = product-led growth signal
**What low DAU/MAU actually means:**
- <20% DAU/MAU for a daily-use product = product isn't integrated into workflow
- DAU/MAU benchmarks vary by use case: email tool (daily use expected) vs. annual budget tool (weekly use is fine)
- Always compare to category, not absolute benchmarks
---
## Engineering Health Benchmarks (CTO)
DORA metrics are the industry standard (Google's DevOps Research and Assessment):
| Metric | Elite | High | Medium | Low |
|--------|-------|------|--------|-----|
| Deployment frequency | Multiple/day | Weekly | Monthly | <Monthly |
| Lead time for changes | <1 hour | 1 day-1 week | 1-6 months | >6 months |
| Change failure rate | <5% | 5-10% | 10-15% | >15% |
| MTTR | <1 hour | <1 day | 1 day-1 week | >1 week |
**Translation for startup stages:**
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| Deploy freq (green) | Weekly | Daily | Daily | Multiple/day |
| MTTR (green) | <4h | <2h | <1h | <30min |
| Change failure rate (green) | <15% | <10% | <7% | <5% |
| Tech debt ratio (green) | <30% | <25% | <20% | <15% |
| P0 incidents/month (green) | <3 | <2 | <2 | <1 |
**Warning signs unique to early-stage:**
- Bus factor = 1 on critical systems (one person knows how it works) → immediate risk
- No on-call rotation → incidents wake the same person every time → attrition risk
- No staging environment → production is the test environment → change failure spike risk
- "We'll fix it after launch" for >12 months → tech debt is now a strategic problem
---
## People Health Benchmarks (CHRO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| Regrettable attrition (green) | <15% | <12% | <10% | <8% |
| Regrettable attrition (red) | >25% | >18% | >15% | >12% |
| eNPS (green) | >20 | >30 | >35 | >40 |
| Time-to-fill (green) | <60d | <45d | <45d | <30d |
| Internal promotion rate | >20% | >25% | >30% | >35% |
| Manager span of control | 1:4-8 | 1:5-8 | 1:6-10 | 1:6-12 |
| % under-performers managed out | 3-5% | 3-5% | 3-5% | 3-5% |
**Regrettable vs non-regrettable attrition:**
- Regrettable: you'd rehire them immediately; they leave for better opportunity
- Non-regrettable: performance-based exits; mutual agreement; role evolution
- Only regrettable attrition signals health problems
**eNPS benchmarks by sector:**
- Tech startups: >30 is good; >50 is exceptional
- General: >0 means more promoters than detractors (minimum bar)
- Below -10: serious cultural issue; expect more attrition
**The cascade warning:** People health is a leading indicator, not lagging. By the time attrition shows up in your numbers, the next wave is already decided. Watch eNPS and engagement quarterly.
---
## Operational Health Benchmarks (COO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| OKR completion rate (green) | >60% | >70% | >75% | >80% |
| Decision cycle time (green) | <3 days | <2 days | <48h | <24h |
| Process maturity level | 1-2 | 2-3 | 3-4 | 4-5 |
| Cross-functional delivery (on time) | >60% | >70% | >75% | >80% |
| Leadership team tenure | N/A | >12mo avg | >18mo avg | >24mo avg |
**OKR interpretation:**
- 100% completion = OKRs were too easy (not ambitious enough)
- 60-70% completion = appropriate stretch, realistic execution
- <40% completion = disconnect between strategy and capacity, or OKRs set without buy-in
- OKRs nobody can remember = OKRs that don't guide decisions = wasted exercise
---
## Security Health Benchmarks (CISO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| Security incidents (P1+) | 0-1/yr | 0/yr | 0/yr | 0/yr |
| Pen test cadence | Annual | Annual | Bi-annual | Bi-annual |
| SOC 2 Type II | Roadmap | In progress | Complete | Complete |
| ISO 27001 | — | Roadmap | In progress | Complete |
| Security training completion | >80% | >90% | >95% | >95% |
| Critical CVE patching SLA | <72h | <48h | <24h | <12h |
| MFA coverage | >80% | >95% | 100% | 100% |
| Employee background checks | Key roles | All | All | All |
**Stage-specific compliance priorities:**
- **Seed:** Basic hygiene (MFA, encryption, access control)
- **Series A:** SOC 2 Type I on roadmap; sales increasingly requiring it
- **Series B:** SOC 2 Type II complete; ISO 27001 if selling to enterprise/EU
- **Series C:** Full compliance stack; GDPR, HIPAA if applicable
---
## Market Health Benchmarks (CMO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| CAC trend | Acceptable | Improving | Improving | Stable/improving |
| Organic % of pipeline | >30% | >40% | >50% | >60% |
| Win rate (green) | >20% | >25% | >27% | >30% |
| Competitive win rate | >40% | >45% | >50% | >55% |
| Brand awareness in ICP | Low OK | Growing | Recognized | Leader |
| Content-to-pipeline conversion | Tracked | >2% | >3% | >4% |
---
## How Dimensions Interact
Understanding interdependencies helps predict cascades before they happen:
```
People Health degrades
↓ (60-90 day lag)
Engineering Health degrades (velocity drops, debt rises)
↓ (30-60 day lag)
Product Health degrades (features slip, quality drops)
↓ (60-90 day lag)
Revenue Health degrades (churn rises, deals stall)
↓ (30-60 day lag)
Financial Health degrades (cash gap, runway shortens)
↓ (immediate)
People Health degrades further (hiring freeze, morale)
```
**The prevention prescription:**
- Fix People and Engineering problems first — they cascade to everything
- Financial problems require immediate response (no lag)
- Revenue problems are often symptoms of Product or People problems upstream
- Security problems can cascade fast (breach → customer churn → financial → people)
**Weighting by stage (for overall score):**
| Dimension | Seed | Series A | Series B | Series C |
|-----------|------|----------|----------|----------|
| Financial | 30% | 25% | 20% | 20% |
| Revenue | 20% | 25% | 25% | 25% |
| People | 20% | 15% | 15% | 15% |
| Product | 15% | 15% | 15% | 15% |
| Engineering | 10% | 10% | 10% | 10% |
| Operations | 5% | 5% | 8% | 8% |
| Market | — | 5% | 5% | 5% |
| Security | — | — | 2% | 2% |
FILE:scripts/health_scorer.py
#!/usr/bin/env python3
"""
Org Health Diagnostic — Multi-Dimension Health Scorer
Scores 8 organizational dimensions on 1-10 scale with traffic lights.
Stdlib only. Run with: python health_scorer.py
"""
import json
import sys
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
from enum import Enum
class Stage(Enum):
SEED = "seed"
SERIES_A = "series_a"
SERIES_B = "series_b"
SERIES_C = "series_c"
class Trend(Enum):
IMPROVING = "improving"
STABLE = "stable"
DECLINING = "declining"
UNKNOWN = "unknown"
class TrafficLight(Enum):
GREEN = "green"
YELLOW = "yellow"
RED = "red"
# Stage weights: how much each dimension contributes to overall score
STAGE_WEIGHTS = {
Stage.SEED: {
"financial": 0.30, "revenue": 0.20, "people": 0.20,
"product": 0.15, "engineering": 0.10, "operations": 0.05,
"market": 0.00, "security": 0.00
},
Stage.SERIES_A: {
"financial": 0.25, "revenue": 0.25, "people": 0.15,
"product": 0.15, "engineering": 0.10, "operations": 0.05,
"market": 0.05, "security": 0.00
},
Stage.SERIES_B: {
"financial": 0.20, "revenue": 0.25, "people": 0.15,
"product": 0.15, "engineering": 0.10, "operations": 0.08,
"market": 0.05, "security": 0.02
},
Stage.SERIES_C: {
"financial": 0.20, "revenue": 0.25, "people": 0.15,
"product": 0.15, "engineering": 0.10, "operations": 0.08,
"market": 0.05, "security": 0.02
},
}
@dataclass
class Metric:
name: str
value: Optional[float]
unit: str
green_threshold: float # value at or above this = green
red_threshold: float # value at or below this = red
higher_is_better: bool = True
def score(self) -> Optional[float]:
"""Score 1-10. Returns None if no value."""
if self.value is None:
return None
v = self.value
g = self.green_threshold
r = self.red_threshold
if self.higher_is_better:
if v >= g:
# Scale 7-10 based on how far above green
excess = min((v - g) / max(g * 0.3, 0.01), 1.0)
return 7.0 + (3.0 * excess)
elif v <= r:
# Scale 1-3 based on how far below red
deficit = min((r - v) / max(r * 0.5, 0.01), 1.0)
return max(1.0, 3.0 - (2.0 * deficit))
else:
# Between red and green → 4-6
if g == r:
return 5.0
position = (v - r) / (g - r)
return 4.0 + (2.0 * position)
else:
# Lower is better — invert
if v <= g:
excess = min((g - v) / max(g * 0.3, 0.01), 1.0)
return 7.0 + (3.0 * excess)
elif v >= r:
deficit = min((v - r) / max(r * 0.5, 0.01), 1.0)
return max(1.0, 3.0 - (2.0 * deficit))
else:
if g == r:
return 5.0
position = (r - v) / (r - g)
return 4.0 + (2.0 * position)
def traffic_light(self) -> Optional[TrafficLight]:
s = self.score()
if s is None:
return None
if s >= 7:
return TrafficLight.GREEN
elif s >= 4:
return TrafficLight.YELLOW
return TrafficLight.RED
@dataclass
class Dimension:
key: str
name: str
owner: str
emoji: str
metrics: List[Metric]
trend: Trend = Trend.UNKNOWN
notes: str = ""
def score(self) -> Optional[float]:
"""Average of available metric scores."""
scores = [m.score() for m in self.metrics if m.score() is not None]
if not scores:
return None
return round(sum(scores) / len(scores), 1)
def traffic_light(self) -> TrafficLight:
s = self.score()
if s is None:
return TrafficLight.YELLOW # Unknown = watch
if s >= 7:
return TrafficLight.GREEN
elif s >= 4:
return TrafficLight.YELLOW
return TrafficLight.RED
def coverage(self) -> float:
"""% of metrics with data."""
filled = sum(1 for m in self.metrics if m.value is not None)
return filled / len(self.metrics) if self.metrics else 0.0
def missing_metrics(self) -> List[str]:
return [m.name for m in self.metrics if m.value is None]
def build_financial_dimension(stage: Stage, **kwargs) -> Dimension:
# Thresholds vary by stage
runway_green = {Stage.SEED: 18, Stage.SERIES_A: 12, Stage.SERIES_B: 12, Stage.SERIES_C: 18}
runway_red = {Stage.SEED: 9, Stage.SERIES_A: 6, Stage.SERIES_B: 6, Stage.SERIES_C: 9}
burn_green = {Stage.SEED: 3.0, Stage.SERIES_A: 2.0, Stage.SERIES_B: 1.5, Stage.SERIES_C: 1.0}
burn_red = {Stage.SEED: 5.0, Stage.SERIES_A: 3.0, Stage.SERIES_B: 2.5, Stage.SERIES_C: 1.5}
return Dimension(
key="financial",
name="Financial Health",
owner="CFO",
emoji="💰",
metrics=[
Metric("Runway (months)", kwargs.get("runway"),
"months", runway_green[stage], runway_red[stage]),
Metric("Burn multiple", kwargs.get("burn_multiple"),
"x", burn_green[stage], burn_red[stage], higher_is_better=False),
Metric("Gross margin (%)", kwargs.get("gross_margin"),
"%", 70, 55),
Metric("MoM growth (%)", kwargs.get("mom_growth"),
"%", 10, 4),
Metric("Revenue concentration (%)", kwargs.get("revenue_concentration"),
"%", 15, 30, higher_is_better=False),
],
trend=kwargs.get("financial_trend", Trend.UNKNOWN),
)
def build_revenue_dimension(stage: Stage, **kwargs) -> Dimension:
nrr_green = {Stage.SEED: 100, Stage.SERIES_A: 110, Stage.SERIES_B: 115, Stage.SERIES_C: 120}
nrr_red = {Stage.SEED: 90, Stage.SERIES_A: 100, Stage.SERIES_B: 105, Stage.SERIES_C: 110}
return Dimension(
key="revenue",
name="Revenue Health",
owner="CRO",
emoji="📈",
metrics=[
Metric("NRR (%)", kwargs.get("nrr"),
"%", nrr_green[stage], nrr_red[stage]),
Metric("Logo churn (%/yr)", kwargs.get("logo_churn"),
"%/yr", 5, 15, higher_is_better=False),
Metric("Pipeline coverage", kwargs.get("pipeline_coverage"),
"x", 3.0, 1.5),
Metric("CAC payback (months)", kwargs.get("cac_payback"),
"months", 12, 24, higher_is_better=False),
Metric("Win rate (%)", kwargs.get("win_rate"),
"%", 25, 15),
],
trend=kwargs.get("revenue_trend", Trend.UNKNOWN),
)
def build_product_dimension(**kwargs) -> Dimension:
return Dimension(
key="product",
name="Product Health",
owner="CPO",
emoji="🚀",
metrics=[
Metric("NPS", kwargs.get("nps"), "score", 40, 20),
Metric("DAU/MAU (%)", kwargs.get("dau_mau"), "%", 35, 15),
Metric("Core feature adoption (%)", kwargs.get("feature_adoption"), "%", 60, 30),
Metric("CSAT", kwargs.get("csat"), "/5", 4.2, 3.5),
Metric("Time-to-value (days)", kwargs.get("ttv_days"), "days", 3, 14, higher_is_better=False),
],
trend=kwargs.get("product_trend", Trend.UNKNOWN),
)
def build_engineering_dimension(**kwargs) -> Dimension:
# Deploy frequency encoded: 5=multiple/day, 4=daily, 3=weekly, 2=monthly, 1=<monthly
return Dimension(
key="engineering",
name="Engineering Health",
owner="CTO",
emoji="⚙️",
metrics=[
Metric("Deploy frequency (1-5)", kwargs.get("deploy_freq"), "scale", 4, 2),
Metric("Change failure rate (%)", kwargs.get("change_failure_rate"), "%", 5, 15, higher_is_better=False),
Metric("MTTR (hours)", kwargs.get("mttr_hours"), "hours", 1, 4, higher_is_better=False),
Metric("Tech debt ratio (%)", kwargs.get("tech_debt_pct"), "%", 15, 35, higher_is_better=False),
Metric("P0/P1 incidents/month", kwargs.get("incidents_monthly"), "count", 1, 5, higher_is_better=False),
],
trend=kwargs.get("engineering_trend", Trend.UNKNOWN),
)
def build_people_dimension(stage: Stage, **kwargs) -> Dimension:
attrition_green = {Stage.SEED: 15, Stage.SERIES_A: 12, Stage.SERIES_B: 10, Stage.SERIES_C: 8}
attrition_red = {Stage.SEED: 25, Stage.SERIES_A: 18, Stage.SERIES_B: 15, Stage.SERIES_C: 12}
return Dimension(
key="people",
name="People Health",
owner="CHRO",
emoji="👥",
metrics=[
Metric("Regrettable attrition (%/yr)", kwargs.get("attrition"),
"%/yr", attrition_green[stage], attrition_red[stage], higher_is_better=False),
Metric("eNPS", kwargs.get("enps"), "score", 30, 0),
Metric("Time-to-fill (days)", kwargs.get("ttf_days"), "days", 45, 90, higher_is_better=False),
Metric("Internal promotion rate (%)", kwargs.get("internal_promo_rate"), "%", 25, 10),
],
trend=kwargs.get("people_trend", Trend.UNKNOWN),
)
def build_operations_dimension(**kwargs) -> Dimension:
return Dimension(
key="operations",
name="Operational Health",
owner="COO",
emoji="🔄",
metrics=[
Metric("OKR completion rate (%)", kwargs.get("okr_completion"), "%", 70, 50),
Metric("Decision cycle time (hours)", kwargs.get("decision_hours"), "hours", 48, 168, higher_is_better=False),
Metric("Process maturity (1-5)", kwargs.get("process_maturity"), "level", 3, 1.5),
Metric("Cross-functional delivery (%)", kwargs.get("xfn_delivery_rate"), "%", 70, 50),
],
trend=kwargs.get("ops_trend", Trend.UNKNOWN),
)
def build_security_dimension(**kwargs) -> Dimension:
return Dimension(
key="security",
name="Security Health",
owner="CISO",
emoji="🔒",
metrics=[
Metric("Security incidents (90 days)", kwargs.get("incidents_90d"), "count", 0, 1, higher_is_better=False),
Metric("MFA coverage (%)", kwargs.get("mfa_coverage"), "%", 95, 80),
Metric("Security training completion (%)", kwargs.get("training_completion"), "%", 95, 80),
Metric("Critical CVE patch rate (%)", kwargs.get("cve_patch_rate"), "%", 100, 85),
Metric("Pen test recency (months)", kwargs.get("pentest_months"), "months", 12, 24, higher_is_better=False),
],
trend=kwargs.get("security_trend", Trend.UNKNOWN),
)
def build_market_dimension(**kwargs) -> Dimension:
return Dimension(
key="market",
name="Market Health",
owner="CMO",
emoji="📣",
metrics=[
Metric("Organic pipeline % ", kwargs.get("organic_pipeline_pct"), "%", 40, 20),
Metric("Competitive win rate (%)", kwargs.get("competitive_win_rate"), "%", 45, 30),
Metric("CAC trend (1=worsening, 5=improving)", kwargs.get("cac_trend_score"), "scale", 4, 2),
],
trend=kwargs.get("market_trend", Trend.UNKNOWN),
)
def calculate_overall(dimensions: List[Dimension], stage: Stage) -> Optional[float]:
weights = STAGE_WEIGHTS[stage]
total_weight = 0.0
weighted_sum = 0.0
for dim in dimensions:
score = dim.score()
w = weights.get(dim.key, 0.0)
if score is not None and w > 0:
weighted_sum += score * w
total_weight += w
if total_weight == 0:
return None
return round(weighted_sum / total_weight, 1)
def trend_arrow(trend: Trend) -> str:
return {
Trend.IMPROVING: "↑",
Trend.STABLE: "→",
Trend.DECLINING: "↓",
Trend.UNKNOWN: "?",
}[trend]
def traffic_light_icon(tl: TrafficLight) -> str:
return {"green": "🟢", "yellow": "🟡", "red": "🔴"}[tl.value]
def print_dashboard(dimensions: List[Dimension], overall: Optional[float],
stage: Stage, company: str = "Company") -> None:
"""Print the full health dashboard."""
print("\n" + "=" * 65)
print(f"ORG HEALTH DIAGNOSTIC — {company.upper()}")
print(f"Stage: {stage.value.replace('_', ' ').title()}")
if overall is not None:
overall_tl = TrafficLight.GREEN if overall >= 7 else (TrafficLight.YELLOW if overall >= 4 else TrafficLight.RED)
print(f"Overall: {traffic_light_icon(overall_tl)} {overall}/10")
print("=" * 65)
print("\nDIMENSION SCORES")
print("─" * 65)
priority_reds = []
priority_yellows = []
for dim in dimensions:
score = dim.score()
tl = dim.traffic_light()
icon = traffic_light_icon(tl)
trend = trend_arrow(dim.trend)
coverage = int(dim.coverage() * 100)
score_str = f"{score:.1f}" if score is not None else "N/A"
cov_str = f"({coverage}% data)" if coverage < 100 else ""
print(f"{dim.emoji} {dim.name:<22} {icon} {score_str:<5} {trend} {dim.owner} {cov_str}")
if tl == TrafficLight.RED and score is not None:
priority_reds.append(dim)
elif tl == TrafficLight.YELLOW and score is not None:
priority_yellows.append(dim)
# Top priorities
if priority_reds or priority_yellows:
print(f"\n{'─' * 65}")
print("PRIORITIES")
print("─" * 65)
idx = 1
for dim in priority_reds[:3]:
print(f"\n🔴 [{idx}] {dim.name} — Score: {dim.score():.1f}/10")
# Show worst metric
worst = min(
[m for m in dim.metrics if m.score() is not None],
key=lambda m: m.score(),
default=None
)
if worst:
print(f" Worst metric: {worst.name} = {worst.value}{worst.unit}")
missing = dim.missing_metrics()
if missing:
print(f" Missing data: {', '.join(missing)}")
idx += 1
for dim in priority_yellows[:2]:
print(f"\n🟡 [{idx}] {dim.name} — Score: {dim.score():.1f}/10 — {trend_arrow(dim.trend)}")
idx += 1
# Data gaps
all_missing = [(dim.name, dim.missing_metrics()) for dim in dimensions if dim.missing_metrics()]
if all_missing:
print(f"\n{'─' * 65}")
print("DATA GAPS (fill to improve diagnostic accuracy)")
for dim_name, metrics in all_missing:
print(f" {dim_name}: {', '.join(metrics)}")
# Cascade warnings
print(f"\n{'─' * 65}")
print("CASCADE RISK")
red_keys = {d.key for d in dimensions if d.traffic_light() == TrafficLight.RED}
if "people" in red_keys:
print(" ⚠️ People RED → Engineering velocity drop expected in 60-90 days")
if "engineering" in red_keys:
print(" ⚠️ Engineering RED → Product quality at risk; roadmap will slip")
if "product" in red_keys:
print(" ⚠️ Product RED → Revenue retention at risk within 2 quarters")
if "revenue" in red_keys:
print(" ⚠️ Revenue RED → Financial pressure mounting; watch runway")
if "financial" in red_keys:
print(" 🚨 Financial RED → All dimensions at risk; immediate board action needed")
if not red_keys:
print(" ✅ No active cascade risks detected")
print(f"\n{'=' * 65}\n")
def to_json(dimensions: List[Dimension], overall: Optional[float], stage: Stage) -> Dict:
result = {
"stage": stage.value,
"overall_score": overall,
"overall_traffic_light": (
TrafficLight.GREEN if overall and overall >= 7
else TrafficLight.YELLOW if overall and overall >= 4
else TrafficLight.RED
).value if overall else "unknown",
"dimensions": {}
}
for dim in dimensions:
result["dimensions"][dim.key] = {
"name": dim.name,
"owner": dim.owner,
"score": dim.score(),
"traffic_light": dim.traffic_light().value,
"trend": dim.trend.value,
"coverage_pct": round(dim.coverage() * 100),
"missing_metrics": dim.missing_metrics(),
"metrics": [
{
"name": m.name,
"value": m.value,
"unit": m.unit,
"score": m.score(),
"traffic_light": m.traffic_light().value if m.traffic_light() else None,
}
for m in dim.metrics
]
}
return result
def build_sample_data(stage: Stage) -> Dict:
"""Sample Series A company data."""
return dict(
# Financial
runway=14, burn_multiple=1.8, gross_margin=68, mom_growth=8.5,
revenue_concentration=28, financial_trend=Trend.STABLE,
# Revenue
nrr=104, logo_churn=8, pipeline_coverage=1.9, cac_payback=16,
win_rate=22, revenue_trend=Trend.DECLINING,
# Product
nps=38, dau_mau=32, feature_adoption=52, csat=4.1,
ttv_days=6, product_trend=Trend.STABLE,
# Engineering
deploy_freq=3, change_failure_rate=9, mttr_hours=2.8,
tech_debt_pct=30, incidents_monthly=2, engineering_trend=Trend.STABLE,
# People
attrition=21, enps=12, ttf_days=58, internal_promo_rate=18,
people_trend=Trend.DECLINING,
# Operations
okr_completion=62, decision_hours=72, process_maturity=2.5,
xfn_delivery_rate=65, ops_trend=Trend.STABLE,
# Security
incidents_90d=0, mfa_coverage=88, training_completion=82,
cve_patch_rate=95, pentest_months=14, security_trend=Trend.IMPROVING,
# Market
organic_pipeline_pct=35, competitive_win_rate=42,
cac_trend_score=3, market_trend=Trend.STABLE,
)
def interactive_mode(stage: Stage) -> Dict:
"""Guided metric entry."""
print("\nEnter metrics (press Enter to skip):\n")
data = {}
def ask(prompt: str, key: str, default=None):
val = input(f" {prompt}: ").strip()
if val:
try:
data[key] = float(val)
except ValueError:
pass
print("💰 FINANCIAL")
ask("Runway (months)", "runway")
ask("Burn multiple (e.g. 1.8)", "burn_multiple")
ask("Gross margin (%)", "gross_margin")
ask("MoM growth (%)", "mom_growth")
ask("Top customer % of ARR", "revenue_concentration")
print("\n📈 REVENUE")
ask("NRR (%)", "nrr")
ask("Logo churn (%/yr)", "logo_churn")
ask("Pipeline coverage (x)", "pipeline_coverage")
ask("CAC payback (months)", "cac_payback")
ask("Win rate (%)", "win_rate")
print("\n🚀 PRODUCT")
ask("NPS score", "nps")
ask("DAU/MAU (%)", "dau_mau")
ask("Core feature adoption (%)", "feature_adoption")
print("\n⚙️ ENGINEERING")
ask("Deploy frequency (1=rare, 5=multiple/day)", "deploy_freq")
ask("Change failure rate (%)", "change_failure_rate")
ask("MTTR (hours)", "mttr_hours")
ask("Tech debt % of sprint", "tech_debt_pct")
print("\n👥 PEOPLE")
ask("Regrettable attrition (%/yr)", "attrition")
ask("eNPS score", "enps")
ask("Time-to-fill (days)", "ttf_days")
print("\n🔄 OPERATIONS")
ask("OKR completion rate (%)", "okr_completion")
print("\n🔒 SECURITY")
ask("MFA coverage (%)", "mfa_coverage")
ask("Security training completion (%)", "training_completion")
return data
def main():
print("\n🏥 ORG HEALTH DIAGNOSTIC")
print("Multi-dimension organizational health scorer\n")
# Determine stage
stage_map = {
"seed": Stage.SEED, "a": Stage.SERIES_A, "series_a": Stage.SERIES_A,
"b": Stage.SERIES_B, "series_b": Stage.SERIES_B,
"c": Stage.SERIES_C, "series_c": Stage.SERIES_C,
}
stage_arg = next((a for a in sys.argv[1:] if a.lower() in stage_map), None)
stage = stage_map.get(stage_arg.lower(), Stage.SERIES_A) if stage_arg else Stage.SERIES_A
if "--interactive" in sys.argv or "-i" in sys.argv:
company = input("Company name: ").strip() or "Company"
stage_input = input("Stage (seed/a/b/c): ").strip().lower()
stage = stage_map.get(stage_input, Stage.SERIES_A)
data = interactive_mode(stage)
else:
print(f"Running sample Series A company data.")
print("(Use --interactive or -i for custom data, --stage seed/a/b/c for stage)\n")
company = "Sample Co"
data = build_sample_data(stage)
# Build dimensions
dimensions = [
build_financial_dimension(stage, **data),
build_revenue_dimension(stage, **data),
build_product_dimension(**data),
build_engineering_dimension(**data),
build_people_dimension(stage, **data),
build_operations_dimension(**data),
build_security_dimension(**data),
build_market_dimension(**data),
]
overall = calculate_overall(dimensions, stage)
print_dashboard(dimensions, overall, stage, company)
if "--json" in sys.argv:
print(json.dumps(to_json(dimensions, overall, stage), indent=2))
if __name__ == "__main__":
main()
Cross-functional what-if modeling for cascading multi-variable scenarios. Unlike single-assumption stress testing, this models compound adversity across all...
---
name: "scenario-war-room"
description: "Cross-functional what-if modeling for cascading multi-variable scenarios. Unlike single-assumption stress testing, this models compound adversity across all business functions simultaneously. Use when facing complex risk scenarios, strategic decisions with major downside, or when the user asks 'what if X AND Y both happen?'"
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: strategic-planning
updated: 2026-03-05
python-tools: scenario_modeler.py
frameworks: scenario-planning
---
# Scenario War Room
Model cascading what-if scenarios across all business functions. Not single-assumption stress tests — compound adversity that shows how one problem creates the next.
## Keywords
scenario planning, war room, what-if analysis, risk modeling, cascading effects, compound risk, adversity planning, contingency planning, stress test, crisis planning, multi-variable scenario, pre-mortem
## Quick Start
```bash
python scripts/scenario_modeler.py # Interactive scenario builder with cascade modeling
```
Or describe the scenario:
```
/war-room "What if we lose our top customer AND miss the Q3 fundraise?"
/war-room "What if 3 engineers quit AND we need to ship by Q3?"
/war-room "What if our market shrinks 30% AND a competitor raises $50M?"
```
## What This Is Not
- **Not** a single-assumption stress test (that's `/em:stress-test`)
- **Not** financial modeling only — every function gets modeled
- **Not** worst-case-only — models 3 severity levels
- **Not** paralysis by analysis — outputs concrete hedges and triggers
## Framework: 6-Step Cascade Model
### Step 1: Define Scenario Variables (max 3)
State each variable with:
- **What changes** — specific, quantified if possible
- **Probability** — your best estimate
- **Timeline** — when it hits
```
Variable A: Top customer (28% ARR) gives 60-day termination notice
Probability: 15% | Timeline: Within 90 days
Variable B: Series A fundraise delayed 6 months beyond target close
Probability: 25% | Timeline: Q3
Variable C: Lead engineer resigns
Probability: 20% | Timeline: Unknown
```
### Step 2: Domain Impact Mapping
For each variable, each relevant role models impact:
| Domain | Owner | Models |
|--------|-------|--------|
| Cash & runway | CFO | Burn impact, runway change, bridge options |
| Revenue | CRO | ARR gap, churn cascade risk, pipeline |
| Product | CPO | Roadmap impact, PMF risk |
| Engineering | CTO | Velocity impact, key person risk |
| People | CHRO | Attrition cascade, hiring freeze implications |
| Operations | COO | Capacity, OKR impact, process risk |
| Security | CISO | Compliance timeline risk |
| Market | CMO | CAC impact, competitive exposure |
### Step 3: Cascade Effect Mapping
This is the core. Show how Variable A triggers consequences in domains that trigger Variable B's effects:
```
TRIGGER: Customer churn ($560K ARR)
↓
CFO: Runway drops 14 → 8 months
↓
CHRO: Hiring freeze; retention risk increases (morale hit)
↓
CTO: 3 open engineering reqs frozen; roadmap slips
↓
CPO: Q4 feature launch delayed → customer retention risk
↓
CRO: NRR drops; existing accounts see reduced velocity → more churn risk
↓
CFO: [Secondary cascade — potential death spiral if not interrupted]
```
Name the cascade explicitly. Show where it can be interrupted.
### Step 4: Severity Matrix
Model three scenarios:
| Scenario | Definition | Recovery |
|----------|------------|---------|
| **Base** | One variable hits; others don't | Manageable with plan |
| **Stress** | Two variables hit simultaneously | Requires significant response |
| **Severe** | All variables hit; full cascade | Existential; requires board intervention |
For each severity level:
- Runway impact
- ARR impact
- Headcount impact
- Timeline to unacceptable state (trigger point)
### Step 5: Trigger Points (Early Warning Signals)
Define the measurable signal that tells you a scenario is unfolding **before** it's confirmed:
```
Trigger for Customer Churn Risk:
- Sponsor goes dark for >3 weeks
- Usage drops >25% MoM
- No Q1 QBR confirmed by Dec 1
Trigger for Fundraise Delay:
- <3 term sheets after 60 days of process
- Lead investor requests >30-day extension on DD
- Competitor raises at lower valuation (market signal)
Trigger for Engineering Attrition:
- Glassdoor activity from engineering team
- 2+ referral interview requests from engineers
- Above-market offer counter-required in last 3 months
```
### Step 6: Hedging Strategies
For each scenario: actions to take **now** (before the scenario materializes) that reduce impact if it does.
| Hedge | Cost | Impact | Owner | Deadline |
|-------|------|--------|-------|---------|
| Establish $500K credit line | $5K/year | Buys 3 months if churn hits | CFO | 60 days |
| 12-month retention bonus for 3 key engineers | $90K | Locks team through fundraise | CHRO | 30 days |
| Diversify to <20% revenue concentration per customer | Sales effort | Reduces single-customer risk | CRO | 2 quarters |
| Compress fundraise timeline, start parallel process | CEO time | Closes before runways merge | CEO | Immediate |
---
## Output Format
Every war room session produces:
```
SCENARIO: [Name]
Variables: [A, B, C]
Most likely path: [which combination actually plays out, with probability]
SEVERITY LEVELS
Base (A only): [runway/ARR impact] — recovery: [X actions]
Stress (A+B): [runway/ARR impact] — recovery: [X actions]
Severe (A+B+C): [runway/ARR impact] — existential risk: [yes/no]
CASCADE MAP
[A → domain impact → B trigger → domain impact → end state]
EARLY WARNING SIGNALS
- [Signal 1 → which scenario it indicates]
- [Signal 2 → which scenario it indicates]
- [Signal 3 → which scenario it indicates]
HEDGES (take these actions now)
1. [Action] — cost: $X — impact: [what it buys] — owner: [role] — deadline: [date]
2. [Action] — cost: $X — impact: [what it buys] — owner: [role] — deadline: [date]
3. [Action] — cost: $X — impact: [what it buys] — owner: [role] — deadline: [date]
RECOMMENDED DECISION
[One paragraph. What to do, in what order, and why.]
```
---
## Rules for Good War Room Sessions
**Max 3 variables per scenario.** More than 3 is noise — you can't meaningfully prepare for 5-variable collapse. Model the 3 that actually worry you.
**Quantify or estimate.** "Revenue drops" is not useful. "$420K ARR at risk over 60 days" is. Use ranges if uncertain.
**Don't stop at first-order effects.** The damage is always in the cascade, not the initial hit.
**Model recovery, not just impact.** Every scenario should have a "what we do" path.
**Separate base case from sensitivity.** Don't conflate "what probably happens" with "what could happen."
**Don't over-model.** 3-4 scenarios per planning cycle is the right number. More creates analysis paralysis.
---
## Common Scenarios by Stage
**Seed:**
- Co-founder leaves + product misses launch
- Funding runs out + bridge terms unfavorable
**Series A:**
- Miss ARR target + fundraise delayed
- Key customer churns + competitor raises
**Series B:**
- Market contraction + burn multiple spikes
- Lead investor wants pivot + team resists
## Integration with C-Suite Roles
| Scenario Type | Primary Roles | Cascade To |
|--------------|---------------|------------|
| Revenue miss | CRO, CFO | CMO (pipeline), COO (cuts), CHRO (layoffs) |
| Key person departure | CHRO, COO | CTO (if eng), CRO (if sales) |
| Fundraise failure | CFO, CEO | COO (runway extension), CHRO (hiring freeze) |
| Security breach | CISO, CTO | CEO (comms), CFO (cost), CRO (customer impact) |
| Market shift | CEO, CPO | CMO (repositioning), CRO (new segments) |
| Competitor move | CMO, CRO | CPO (roadmap response), CEO (strategy) |
## References
- `references/scenario-planning.md` — Shell methodology, pre-mortem, Monte Carlo, cascade frameworks
- `scripts/scenario_modeler.py` — CLI tool for structured scenario modeling
FILE:references/scenario-planning.md
# Scenario Planning Reference
## Shell's Scenario Planning Methodology
Shell invented modern scenario planning in the 1970s after the oil crisis. Core insight: **scenarios are not forecasts — they're tools for thinking.**
### Shell's Principles (adapted for startups)
1. **Scenarios are mutually exclusive, collectively exhaustive** — they cover the space of possibilities without overlapping
2. **2x2 matrix** — pick 2 critical uncertainties (not risks — uncertainties); cross them to get 4 scenarios
3. **Name the scenarios** — named scenarios are remembered; numbered ones aren't
4. **Identify predetermined elements** — things that will happen regardless of scenario (regulatory changes, tech trends)
5. **Early indicators** — each scenario has signals you can monitor today
### Shell's 2x2 for Startups
Critical uncertainties for early-stage SaaS:
| | Market grows fast | Market grows slow |
|---|---|---|
| **We raise successfully** | "Blue Ocean" — execute hard | "Ramp Carefully" — efficiency focus |
| **We bridge/delay raise** | "Scrappy Growth" — ramen profitability | "Survival Mode" — cut to core |
Build your war room sessions around whichever quadrant is most relevant right now.
---
## Monte Carlo Thinking for Startups
Monte Carlo = running thousands of simulations with random variables to understand probability distributions.
You don't need software. Apply the mental model:
### The Mental Monte Carlo Process
1. **Identify the key variables** (3-5 max)
2. **Assign ranges** — not point estimates
- CAC: $6K–$12K (uniform distribution)
- Close rate: 20%–40% (normal, mean 30%)
- Churn: 5%–20% (right-skewed — bad tail is worse)
3. **Run mental scenarios** — pick low/mid/high for each
4. **Identify the combinations that kill you** — which variable combinations make runway hit zero?
5. **Focus hedging on** the 20% of combinations that account for 80% of kill scenarios
### Practical Monte Carlo Heuristic
For revenue forecasting, always state:
- **P90** (90% confidence you'll exceed this)
- **P50** (median case)
- **P10** (only 10% chance you'll exceed this — your "stretch")
Boards respect ranges. Point estimates are usually wrong and make you look naive.
---
## Pre-Mortem Technique
A pre-mortem asks: *"It's 12 months from now. We failed. Why?"*
It's the opposite of planning (which asks why you'll succeed). It surfaces hidden risks that optimism suppresses.
### Running a Pre-Mortem
**Setup:**
- Time: 90 minutes
- Participants: leadership team
- Facilitator: neutral (COO, or external)
- Assumption: "It's [date 12 months out]. The company failed / missed its major goal. This is real."
**Phase 1 — Silence (10 minutes):**
Each person writes their top 3 reasons the failure happened. No discussion.
**Phase 2 — Round Robin (30 minutes):**
Each person shares one reason per turn. Facilitator captures on whiteboard. No debate yet.
**Phase 3 — Cluster (20 minutes):**
Group similar causes. Identify the top 5 clusters.
**Phase 4 — Probability & Impact (20 minutes):**
For each cluster: P(likely) × impact = risk score. Rank.
**Phase 5 — Mitigation (10 minutes):**
Top 3 risks: what one action would most reduce each?
### Pre-Mortem Prompt Variants
- "It's March 2027. We ran out of money. Why?"
- "It's Q4. We lost 3 enterprise customers in 60 days. What happened?"
- "It's next year. Our top competitor took 40% of the market. How?"
- "It's 18 months from now. Half the engineering team left. What triggered it?"
---
## Cascade Effect Mapping
Cascades are where most startups get surprised. The first hit is expected — the second and third aren't.
### Cascade Mapping Format
Draw as a chain:
```
INITIAL EVENT
↓ [immediate effect: domain, severity, timeline]
SECONDARY EFFECT
↓ [cascade mechanism: how A causes B]
TERTIARY EFFECT
↓ [cascade mechanism]
END STATE [runway impact, ARR impact, team impact]
```
### Common Cascade Patterns
**Revenue → Cash → People:**
```
Customer churns ($400K ARR)
↓ CFO: runway drops 14→9 months; bridge needed
↓ CHRO: hiring freeze; morale drops; attrition risk
↓ CTO: roadmap slips; key engineers leave for certainty
↓ CPO: product quality drops; more churn risk
↓ CRO: harder to win new logos without product velocity
END STATE: Death spiral if not interrupted at step 2
```
**Fundraise → Operations → Product:**
```
Fundraise delayed 6 months
↓ CFO: bridge at unfavorable terms; equity dilution
↓ COO: freeze all non-essential spend; process degrades
↓ CPO: roadmap cut to 40% of planned scope
↓ CTO: no infra investment; tech debt accelerates
↓ CRO: product gaps start losing deals to feature-complete competitors
END STATE: Weaker position at next raise; lower valuation
```
**People → Product → Revenue:**
```
Lead engineer + 2 seniors leave (30% of eng team)
↓ CTO: velocity drops 50%; critical features slip Q3→Q4
↓ CPO: Q4 launch cancelled; roadmap confidence collapses
↓ CRO: 3 enterprise deals cite product timeline → delays/losses
↓ CFO: $600K pipeline at risk; raises needed earlier
END STATE: Fundraise from position of weakness; team morale spiral
```
### Identifying Cascade Break Points
Every cascade has a point where intervention is cheapest. Find it:
- Step 1: Very expensive to prevent (existential)
- Step 2: Moderate cost (management action)
- Step 3: Cheap (early signal response)
Always try to interrupt at Step 2 or earlier.
---
## Trigger-Based Contingency Plans
Triggers are measurable signals you commit to acting on **before** the scenario fully materializes.
### Trigger Design Principles
1. **Measurable** — not "things look bad" but "cash below $800K"
2. **Leading, not lagging** — triggers should fire 60-90 days before the crisis
3. **Pre-committed responses** — when trigger fires, the action is already decided
4. **Owner assigned** — who watches for this trigger?
### Trigger Examples
**Cash / Runway:**
```
Trigger: Cash drops below $1M (or runway < 6 months)
Pre-committed response:
- CFO: activate credit line within 48 hours
- CEO: begin bridge conversations with existing investors
- COO: implement 20% spend reduction plan (already drafted)
Owner: CFO (weekly cash report to CEO)
```
**Customer Health:**
```
Trigger: Any customer >10% ARR shows 3 of: [sponsor gone dark, usage -25%,
no renewal discussion by 90 days before contract end, missed QBR]
Pre-committed response:
- CRO: executive escalation call within 48 hours
- CPO: product health review scheduled
- CEO: direct outreach if escalation fails
Owner: CRO (health score dashboard, weekly)
```
**Fundraise:**
```
Trigger: <3 term sheets after 8 weeks of active process
Pre-committed response:
- CEO: expand process to 10 additional firms
- CFO: model bridge scenarios; draft bridge terms
- COO: prepare 90-day cost reduction plan
Owner: CEO (weekly fundraise status)
```
---
## How Many Scenarios to Model
**Answer: 3-4 max per planning cycle.**
The math: 3 scenarios × 6 domains × 3 severity levels = 54 combinations. That's already overwhelming. More scenarios don't improve decisions — they paralyze them.
### The Right 3-4 Scenarios
1. **Most likely adverse scenario** — what actually keeps you up at night
2. **Market/macro scenario** — something outside your control
3. **Black swan** — low probability, existential if it hits
4. **Compound scenario** — your top 2 adverse events happening simultaneously
### What Kills Scenario Planning
- **Too many scenarios** — decision paralysis
- **Only modeling what's comfortable** — survivorship bias
- **No pre-committed responses** — it's just worry, not planning
- **Not revisiting** — scenarios from 12 months ago are often irrelevant
- **Treating scenarios as forecasts** — they're possibilities, not predictions
- **Confusing risk with uncertainty** — risk has known probabilities; uncertainty doesn't
FILE:scripts/scenario_modeler.py
#!/usr/bin/env python3
"""
Scenario War Room — Multi-Variable Cascade Modeler
Models cascading effects of compound adversity across business domains.
Stdlib only. Run with: python scenario_modeler.py
"""
import json
import sys
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
from enum import Enum
class Severity(Enum):
BASE = "base" # One variable hits
STRESS = "stress" # Two variables hit
SEVERE = "severe" # All variables hit
class Domain(Enum):
FINANCIAL = "Financial (CFO)"
REVENUE = "Revenue (CRO)"
PRODUCT = "Product (CPO)"
ENGINEERING = "Engineering (CTO)"
PEOPLE = "People (CHRO)"
OPERATIONS = "Operations (COO)"
SECURITY = "Security (CISO)"
MARKET = "Market (CMO)"
@dataclass
class Variable:
name: str
description: str
probability: float # 0.0-1.0
arrt_impact_pct: float # % of ARR at risk (negative = loss)
runway_impact_months: float # months lost from runway (negative = reduction)
affected_domains: List[Domain]
timeline_days: int # when it hits
@dataclass
class CascadeEffect:
trigger_domain: Domain
caused_domain: Domain
mechanism: str # how A causes B
severity_multiplier: float # compounds the base impact
@dataclass
class Hedge:
action: str
cost_usd: int
impact_description: str
owner: str
deadline_days: int
reduces_probability: float # how much it reduces scenario probability
@dataclass
class Scenario:
name: str
variables: List[Variable]
cascades: List[CascadeEffect]
hedges: List[Hedge]
# Company baseline
current_arr_usd: int = 2_000_000
current_runway_months: int = 14
monthly_burn_usd: int = 140_000
def calculate_impact(
scenario: Scenario,
severity: Severity
) -> Dict:
"""Calculate combined impact for a given severity level."""
variables = scenario.variables
# Select variables by severity
if severity == Severity.BASE:
active_vars = variables[:1]
elif severity == Severity.STRESS:
active_vars = variables[:2]
else:
active_vars = variables
# Direct impacts
total_arr_loss_pct = sum(abs(v.arrt_impact_pct) for v in active_vars)
total_runway_reduction = sum(abs(v.runway_impact_months) for v in active_vars)
arr_at_risk = scenario.current_arr_usd * (total_arr_loss_pct / 100)
new_arr = scenario.current_arr_usd - arr_at_risk
new_runway = scenario.current_runway_months - total_runway_reduction
# Cascade multiplier (stress/severe amplify via domain cascades)
cascade_multiplier = 1.0
if len(active_vars) > 1:
active_domains = set(d for v in active_vars for d in v.affected_domains)
for cascade in scenario.cascades:
if (cascade.trigger_domain in active_domains and
cascade.caused_domain in active_domains):
cascade_multiplier *= cascade.severity_multiplier
# Apply cascade
effective_arr_loss = arr_at_risk * cascade_multiplier
effective_arr = scenario.current_arr_usd - effective_arr_loss
effective_runway = max(0, new_runway - (cascade_multiplier - 1.0) * 2)
# New burn multiple
new_monthly_burn = scenario.monthly_burn_usd * cascade_multiplier
burn_multiple = (new_monthly_burn * 12) / max(effective_arr, 1)
# Affected domains
affected = set(d for v in active_vars for d in v.affected_domains)
return {
"severity": severity.value,
"active_variables": [v.name for v in active_vars],
"arr_at_risk_usd": int(effective_arr_loss),
"arr_at_risk_pct": round(effective_arr_loss / scenario.current_arr_usd * 100, 1),
"projected_arr_usd": int(effective_arr),
"runway_months": round(effective_runway, 1),
"runway_change": round(effective_runway - scenario.current_runway_months, 1),
"cascade_multiplier": round(cascade_multiplier, 2),
"new_burn_multiple": round(burn_multiple, 1),
"affected_domains": [d.value for d in affected],
"existential_risk": effective_runway < 6.0,
"board_escalation_required": effective_runway < 9.0,
}
def identify_triggers(variables: List[Variable]) -> List[Dict]:
"""Generate early warning triggers for each variable."""
triggers = []
for var in variables:
trigger = {
"variable": var.name,
"timeline": f"Watch from day 1; expect signal ~{var.timeline_days // 2} days before impact",
"signals": _generate_signals(var),
"response_owner": _domain_to_owner(var.affected_domains[0] if var.affected_domains else Domain.FINANCIAL),
}
triggers.append(trigger)
return triggers
def _generate_signals(var: Variable) -> List[str]:
"""Generate plausible early warning signals based on variable type."""
signals = []
name_lower = var.name.lower()
if any(k in name_lower for k in ["customer", "churn", "account"]):
signals = [
"Executive sponsor unreachable for >2 weeks",
"Product usage drops >20% month-over-month",
"No QBR scheduled within 90 days of contract renewal",
"Support ticket volume spikes >50% without explanation",
]
elif any(k in name_lower for k in ["fundraise", "raise", "capital", "investor"]):
signals = [
"Fewer than 3 term sheets after 60 days of active process",
"Lead investor requests 30+ day extension on diligence",
"Comparable company raises at lower valuation (market signal)",
"Investor meeting conversion rate below 20%",
]
elif any(k in name_lower for k in ["engineer", "people", "team", "resign", "quit"]):
signals = [
"2+ engineers receive above-market counter-offer in 90 days",
"Glassdoor activity increases from engineering team",
"Key person requests 1:1 to 'talk about career' unexpectedly",
"Referral interview requests from engineers increase",
]
elif any(k in name_lower for k in ["market", "competitor", "competition"]):
signals = [
"Competitor raises $10M+ funding round",
"Win/loss rate shifts >10% in 60 days",
"Multiple prospects cite competitor by name in objections",
"Competitor poaches 2+ of your customers in a quarter",
]
else:
signals = [
f"Leading indicator for '{var.name}' deteriorates 20%+ vs baseline",
"Weekly metric review shows 3-week trend in wrong direction",
"External validation from customers or partners confirms risk",
]
return signals[:3] # Top 3
def _domain_to_owner(domain: Domain) -> str:
mapping = {
Domain.FINANCIAL: "CFO",
Domain.REVENUE: "CRO",
Domain.PRODUCT: "CPO",
Domain.ENGINEERING: "CTO",
Domain.PEOPLE: "CHRO",
Domain.OPERATIONS: "COO",
Domain.SECURITY: "CISO",
Domain.MARKET: "CMO",
}
return mapping.get(domain, "CEO")
def format_currency(amount: int) -> str:
if amount >= 1_000_000:
return f".1fM"
elif amount >= 1_000:
return f".0fK"
return f"amount"
def print_report(scenario: Scenario) -> None:
"""Print full scenario analysis report."""
print("\n" + "=" * 70)
print(f"SCENARIO WAR ROOM: {scenario.name.upper()}")
print("=" * 70)
# Baseline
print(f"\n📊 BASELINE")
print(f" Current ARR: {format_currency(scenario.current_arr_usd)}")
print(f" Monthly Burn: {format_currency(scenario.monthly_burn_usd)}")
print(f" Runway: {scenario.current_runway_months} months")
# Variables
print(f"\n⚡ SCENARIO VARIABLES ({len(scenario.variables)})")
for i, var in enumerate(scenario.variables, 1):
prob_pct = int(var.probability * 100)
print(f"\n Variable {i}: {var.name}")
print(f" {var.description}")
print(f" Probability: {prob_pct}% | Timeline: {var.timeline_days} days")
print(f" ARR impact: -{var.arrt_impact_pct}% | "
f"Runway impact: -{var.runway_impact_months} months")
print(f" Affected: {', '.join(d.value for d in var.affected_domains)}")
# Combined probability
combined_prob = 1.0
for var in scenario.variables:
combined_prob *= var.probability
print(f"\n Combined probability (all hit): {combined_prob * 100:.1f}%")
# Severity Levels
print(f"\n{'=' * 70}")
print("SEVERITY ANALYSIS")
print("=" * 70)
for severity in Severity:
if severity == Severity.BASE and len(scenario.variables) < 1:
continue
if severity == Severity.STRESS and len(scenario.variables) < 2:
continue
impact = calculate_impact(scenario, severity)
icon = {"base": "🟡", "stress": "🔴", "severe": "💀"}[impact["severity"]]
print(f"\n{icon} {impact['severity'].upper()} SCENARIO")
print(f" Variables: {', '.join(impact['active_variables'])}")
print(f" ARR at risk: {format_currency(impact['arr_at_risk_usd'])} "
f"({impact['arr_at_risk_pct']}%)")
print(f" Projected ARR: {format_currency(impact['projected_arr_usd'])}")
print(f" Runway: {impact['runway_months']} months "
f"({impact['runway_change']:+.1f} months)")
print(f" Burn multiple: {impact['new_burn_multiple']}x")
if impact['cascade_multiplier'] > 1.0:
print(f" Cascade amplifier: {impact['cascade_multiplier']}x "
f"(domains interact)")
print(f" Board escalation: {'⚠️ YES' if impact['board_escalation_required'] else 'No'}")
print(f" Existential risk: {'🚨 YES' if impact['existential_risk'] else 'No'}")
# Cascade Map
if scenario.cascades:
print(f"\n{'=' * 70}")
print("CASCADE MAP")
print("=" * 70)
for i, cascade in enumerate(scenario.cascades, 1):
print(f"\n [{i}] {cascade.trigger_domain.value}")
print(f" ↓ {cascade.mechanism}")
print(f" → {cascade.caused_domain.value} "
f"(amplified {cascade.severity_multiplier}x)")
# Early Warning Triggers
print(f"\n{'=' * 70}")
print("EARLY WARNING TRIGGERS")
print("=" * 70)
triggers = identify_triggers(scenario.variables)
for trigger in triggers:
print(f"\n 📡 {trigger['variable']}")
print(f" Watch: {trigger['timeline']}")
print(f" Owner: {trigger['response_owner']}")
for signal in trigger['signals']:
print(f" • {signal}")
# Hedges
if scenario.hedges:
print(f"\n{'=' * 70}")
print("HEDGING STRATEGIES (act now)")
print("=" * 70)
sorted_hedges = sorted(scenario.hedges,
key=lambda h: h.reduces_probability, reverse=True)
for hedge in sorted_hedges:
print(f"\n ✅ {hedge.action}")
print(f" Cost: {format_currency(hedge.cost_usd)}/year | "
f"Owner: {hedge.owner} | Deadline: {hedge.deadline_days} days")
print(f" Impact: {hedge.impact_description}")
print(f" Risk reduction: {int(hedge.reduces_probability * 100)}%")
print(f"\n{'=' * 70}\n")
def build_sample_scenario() -> Scenario:
"""Sample: Customer churn + fundraise miss compound scenario."""
variables = [
Variable(
name="Top customer churn",
description="Largest customer (28% of ARR) gives 60-day termination notice",
probability=0.15,
arrt_impact_pct=28.0,
runway_impact_months=4.0,
affected_domains=[
Domain.FINANCIAL, Domain.REVENUE, Domain.OPERATIONS
],
timeline_days=60,
),
Variable(
name="Series A delayed 6 months",
description="Fundraise process extends beyond target close; bridge required",
probability=0.25,
arrt_impact_pct=0.0, # No ARR impact directly
runway_impact_months=3.0, # Bridge terms reduce effective runway
affected_domains=[
Domain.FINANCIAL, Domain.PEOPLE, Domain.OPERATIONS
],
timeline_days=120,
),
Variable(
name="Lead engineer resigns",
description="Engineering lead + 1 senior resign during uncertainty",
probability=0.20,
arrt_impact_pct=5.0, # Roadmap slip causes some revenue impact
runway_impact_months=1.0,
affected_domains=[
Domain.ENGINEERING, Domain.PRODUCT, Domain.REVENUE
],
timeline_days=30,
),
]
cascades = [
CascadeEffect(
trigger_domain=Domain.REVENUE,
caused_domain=Domain.FINANCIAL,
mechanism="ARR loss increases burn multiple; runway compresses",
severity_multiplier=1.3,
),
CascadeEffect(
trigger_domain=Domain.FINANCIAL,
caused_domain=Domain.PEOPLE,
mechanism="Hiring freeze + uncertainty triggers attrition risk",
severity_multiplier=1.2,
),
CascadeEffect(
trigger_domain=Domain.PEOPLE,
caused_domain=Domain.PRODUCT,
mechanism="Engineering attrition slips roadmap; customer value drops",
severity_multiplier=1.15,
),
]
hedges = [
Hedge(
action="Establish $750K revolving credit line",
cost_usd=7_500,
impact_description="Buys 4+ months if churn hits before fundraise closes",
owner="CFO",
deadline_days=45,
reduces_probability=0.40,
),
Hedge(
action="12-month retention bonuses for 3 key engineers",
cost_usd=90_000,
impact_description="Locks critical talent through fundraise uncertainty",
owner="CHRO",
deadline_days=30,
reduces_probability=0.60,
),
Hedge(
action="Diversify revenue: reduce top customer to <20% ARR in 2 quarters",
cost_usd=0,
impact_description="Structural risk reduction; takes 6+ months to achieve",
owner="CRO",
deadline_days=14,
reduces_probability=0.30,
),
Hedge(
action="Accelerate fundraise: start parallel process, compress timeline",
cost_usd=15_000,
impact_description="Closes before scenarios compound; reduces bridge risk",
owner="CEO",
deadline_days=7,
reduces_probability=0.35,
),
]
return Scenario(
name="Customer Churn + Fundraise Miss + Eng Attrition",
variables=variables,
cascades=cascades,
hedges=hedges,
current_arr_usd=2_000_000,
current_runway_months=14,
monthly_burn_usd=140_000,
)
def interactive_mode() -> Scenario:
"""Simple CLI for building a custom scenario."""
print("\n🔴 SCENARIO WAR ROOM — Custom Scenario Builder")
print("=" * 50)
print("Define up to 3 scenario variables.\n")
name = input("Scenario name: ").strip() or "Custom Scenario"
current_arr = int(input("Current ARR ($): ").strip() or "2000000")
current_runway = int(input("Current runway (months): ").strip() or "14")
monthly_burn = int(current_arr / current_runway) if current_runway > 0 else 140000
variables = []
for i in range(1, 4):
print(f"\nVariable {i} (press Enter to skip):")
var_name = input(" Name: ").strip()
if not var_name:
break
desc = input(" Description: ").strip() or var_name
prob = float(input(" Probability (0-100%): ").strip() or "20") / 100
arr_impact = float(input(" ARR impact (%): ").strip() or "10")
runway_impact = float(input(" Runway impact (months): ").strip() or "2")
timeline = int(input(" Timeline (days): ").strip() or "90")
variables.append(Variable(
name=var_name,
description=desc,
probability=prob,
arrt_impact_pct=arr_impact,
runway_impact_months=runway_impact,
affected_domains=[Domain.FINANCIAL, Domain.REVENUE],
timeline_days=timeline,
))
if not variables:
print("No variables defined. Using sample scenario.")
return build_sample_scenario()
return Scenario(
name=name,
variables=variables,
cascades=[],
hedges=[],
current_arr_usd=current_arr,
current_runway_months=current_runway,
monthly_burn_usd=monthly_burn,
)
def main():
print("\n🔴 SCENARIO WAR ROOM")
print("Multi-variable cascade modeler for startup adversity planning\n")
if "--interactive" in sys.argv or "-i" in sys.argv:
scenario = interactive_mode()
else:
print("Running sample scenario: Customer Churn + Fundraise Miss + Eng Attrition")
print("(Use --interactive or -i for custom scenario)\n")
scenario = build_sample_scenario()
print_report(scenario)
if "--json" in sys.argv:
results = {}
for severity in Severity:
impact = calculate_impact(scenario, severity)
results[severity.value] = impact
print(json.dumps(results, indent=2))
if __name__ == "__main__":
main()
Assembles comprehensive board and investor update decks by pulling perspectives from all C-suite roles. Use when preparing board meetings, investor updates,...
--- name: "board-deck-builder" description: "Assembles comprehensive board and investor update decks by pulling perspectives from all C-suite roles. Use when preparing board meetings, investor updates, quarterly business reviews, or fundraising narratives. Covers structure, narrative framework, bad news delivery, and common mistakes." license: MIT metadata: version: 1.0.0 author: Alireza Rezvani category: c-level domain: board-governance updated: 2026-03-05 frameworks: deck-frameworks, board-deck-template --- # Board Deck Builder Build board decks that tell a story — not just show data. Every section has an owner, a narrative, and a "so what." ## Keywords board deck, investor update, board meeting, board pack, investor relations, quarterly review, board presentation, fundraising deck, investor deck, board narrative, QBR, quarterly business review ## Quick Start ``` /board-deck [quarterly|monthly|fundraising] [stage: seed|seriesA|seriesB] ``` Provide available metrics. The builder fills gaps with explicit placeholders — never invents numbers. ## Deck Structure (Standard Order) Every section follows: **Headline → Data → Narrative → Ask/Next** ### 1. Executive Summary (CEO) **3 sentences. No more.** - Sentence 1: State of the business (where we are) - Sentence 2: Biggest thing that happened this period - Sentence 3: Where we're going next quarter *Bad:* "We had a good quarter with lots of progress across all areas." *Good:* "We closed Q3 at $2.4M ARR (+22% QoQ), signed our largest enterprise contract, and enter Q4 with 14-month runway. The strategic shift to mid-market is working — ACV up 40% and sales cycle down 3 weeks. Q4 priority: close the $3M Series A and hit $2.8M ARR." ### 2. Key Metrics Dashboard (COO) **6-8 metrics max. Use a table.** | Metric | This Period | Last Period | Target | Status | |--------|-------------|-------------|--------|--------| | ARR | $2.4M | $1.97M | $2.3M | ✅ | | MoM growth | 8.1% | 7.2% | 7.5% | ✅ | | Burn multiple | 1.8x | 2.1x | <2x | ✅ | | NRR | 112% | 108% | >110% | ✅ | | CAC payback | 11 months | 14 months | <12 months | ✅ | | Headcount | 24 | 21 | 25 | 🟡 | Pick metrics the board actually tracks. Swap out anything they've said they don't care about. ### 3. Financial Update (CFO) - P&L summary: Revenue, COGS, Gross margin, OpEx, Net burn - Cash position and runway (months) - Burn multiple trend (3-quarter view) - Variance to plan (what was different and why) - Forecast update for next quarter **One sentence on each variance.** Boards hate "revenue was below target" with no explanation. Say why. ### 4. Revenue & Pipeline (CRO) - ARR waterfall: starting → new → expansion → churn → ending - NRR and logo churn rates - Pipeline by stage (in $, not just count) - Forecast: next quarter with confidence level - Top 3 deals: name/amount/close date/risk **The forecast must have a confidence level.** "We expect $2.8M" is weak. "High confidence $2.6M, upside to $2.9M if two late-stage deals close" is useful. ### 5. Product Update (CPO) - Shipped this quarter: 3-5 bullets, user impact for each - Shipping next quarter: 3-5 bullets with target dates - PMF signal: NPS trend, DAU/MAU ratio, feature adoption - One key learning from customer research **No feature lists.** Only features with evidence of user impact. ### 6. Growth & Marketing (CMO) - CAC by channel (table) - Pipeline contribution by channel ($) - Brand/awareness metrics relevant to stage (traffic, share of voice) - What's working, what's being cut, what's being tested ### 7. Engineering & Technical (CTO) - Delivery velocity trend (last 4 quarters) - Tech debt ratio and plan - Infrastructure: uptime, incidents, cost trend - Security posture (one line, flag anything pending) **Keep this short unless there's a material issue.** Boards don't need sprint details. ### 8. Team & People (CHRO) - Headcount: actual vs plan - Hiring: offers out, pipeline, time-to-fill trend - Attrition: regrettable vs non-regrettable - Engagement: last survey score, trend - Key hires this quarter, key open roles ### 9. Risk & Security (CISO) - Security posture: status of critical controls - Compliance: certifications in progress, deadlines - Incidents this quarter (if any): impact, resolution, prevention - Top 3 risks and mitigation status ### 10. Strategic Outlook (CEO) - Next quarter priorities: 3-5 items, ranked - Key decisions needed from the board - Asks: budget, introductions, advice, votes **The "asks" slide is the most important.** Be specific. "We'd like 3 warm introductions to CFOs at Series B companies" beats "any help would be appreciated." ### 11. Appendix - Detailed financial model - Full pipeline data - Cohort retention charts - Customer case studies - Detailed headcount breakdown --- ## Narrative Framework Boards see 10+ decks per quarter. Yours needs a through-line. **The 4-Act Structure:** 1. **Where we said we'd be** (last quarter's targets) 2. **Where we actually are** (honest assessment) 3. **Why the gap exists** (one cause per variance, not excuses) 4. **What we're doing about it** (specific, dated actions) This works for good news AND bad news. It's credible because it acknowledges reality. **Opening frame:** Start with the one thing that matters most — the board should know the key message by slide 3, not slide 30. --- ## Delivering Bad News Never bury it. Boards find out eventually. Finding out late makes it worse. **Framework:** 1. **State it plainly** — "We missed Q3 ARR target by $300K (12% gap)" 2. **Own the cause** — "Primary driver was longer-than-expected sales cycle in enterprise segment" 3. **Show you understand it** — "We analyzed 8 lost/stalled deals; the pattern is X" 4. **Present the fix** — "We've made 3 changes: [specific, dated changes]" 5. **Update the forecast** — "Revised Q4 target is $2.6M; here's the bottom-up build" **What NOT to do:** - Don't lead with good news to soften bad news — boards notice and distrust the framing - Don't explain without owning — "market conditions" is not a cause, it's a context - Don't present a fix without data behind it - Don't show a revised forecast without showing your assumptions --- ## Common Board Deck Mistakes | Mistake | Fix | |---------|-----| | Too many slides (>25) | Cut ruthlessly — if you can't explain it in the room, the slide is wrong | | Metrics without targets | Every metric needs a target and a status | | No narrative | Data without story forces boards to draw their own conclusions | | Burying bad news | Lead with it, own it, fix it | | Vague asks | Specific, actionable, person-assigned asks only | | No variance explanation | Every gap from target needs one-sentence cause | | Stale appendix | Appendix is only useful if it's current | | Designing for the reader, not the room | Decks are presented — they must work spoken aloud | --- ## Cadence Notes **Quarterly (standard):** Full deck, all sections, 20-30 slides. Sent 48 hours in advance. **Monthly (for early-stage):** Condensed — metrics dashboard, financials, pipeline, top risks. 8-12 slides. **Fundraising:** Opens with market/vision, closes with ask. See `references/deck-frameworks.md` for Sequoia format. ## References - `references/deck-frameworks.md` — SaaS board pack format, Sequoia structure, investor tailoring - `templates/board-deck-template.md` — fill-in template for complete board decks FILE:references/deck-frameworks.md # Board Deck Frameworks ## The SaaS Board Pack (Christoph Janz / Point Nine Style) Point Nine's board pack format became the de facto standard for early-stage SaaS. Core principle: **the numbers tell the story; the narrative explains the numbers.** ### Required Metrics (non-negotiable for SaaS boards) - **ARR** (not MRR — boards think annually) - **MoM / QoQ growth rate** - **NRR (Net Revenue Retention)** — the single most important SaaS metric - **Gross margin** — typically 60-80% SaaS; <60% is a flag - **CAC payback period** — months to recover customer acquisition cost - **Burn multiple** = net burn / net new ARR; <2x is good, >3x is a problem - **Runway** — months at current burn ### Point Nine Benchmark Targets (Series A SaaS) | Metric | Good | Great | Warning | |--------|------|-------|---------| | MoM growth | 10-15% | >20% | <7% | | NRR | >110% | >130% | <100% | | Gross margin | >65% | >75% | <60% | | CAC payback | <18 months | <12 months | >24 months | | Burn multiple | <2x | <1.5x | >3x | | Logo churn | <10%/yr | <5%/yr | >15%/yr | ### SaaS ARR Waterfall (Christoph Janz Format) Show this every quarter: ``` Starting ARR: $1,970,000 + New ARR: +$480,000 (new logos) + Expansion ARR: +$120,000 (upsells/cross-sells) - Churned ARR: -$90,000 (cancellations) - Contraction ARR: -$35,000 (downgrades) = Ending ARR: $2,445,000 ``` NRR = (Ending - New) / Starting = ($1,965K) / ($1,970K) = 99.7% ← flag this --- ## Sequoia Board Deck Structure Sequoia's canonical deck (used for both fundraising and board updates): 1. **Company Purpose** — one sentence, the existential "why" 2. **The Problem** — pain, size, who has it 3. **The Solution** — what you do, how it's different 4. **Why Now** — market timing, tailwinds, enabling factors 5. **Market Size** — TAM/SAM/SOM with methodology 6. **Business Model** — how you make money 7. **Traction** — proof it's working (growth, retention, logos) 8. **Team** — why you're the ones to win this 9. **Financials** — 3-year model, current metrics 10. **The Ask** — amount, use of funds, milestones to next round **For ongoing board updates:** Swap 1-5 (context) for "State of the Business" and "Last Quarter vs Plan." Boards know the company — skip the pitch. --- ## Investor-Specific Tailoring ### What Different Investor Types Care About **Early-stage VCs (Seed, A):** - Growth rate above all else - NRR — "does the product retain?" - Founder-market fit narrative - Milestone achievement vs last board meeting **Growth-stage VCs (B, C):** - Capital efficiency (burn multiple, CAC payback) - GTM repeatability — can you hire 10 AEs and have it work? - Market leadership signals - Path to profitability (even if years away) **Strategic investors:** - Synergies with their portfolio/business - Technology differentiation - Partnership potential **Angels:** - Team above all - Personal conviction in the thesis - Exit scenarios ### Tailoring the Narrative - If you're ahead of plan: "Here's why, and here's how we'll sustain it" - If you're behind plan: "Here's why, here's what we've learned, here's the new plan" - If the plan was wrong: "The assumption that was wrong, what we know now, updated thesis" Never pretend the plan was right when it wasn't. Board members have memories and models. --- ## How to Present Bad News Boards have seen everything. What loses credibility isn't bad results — it's bad framing. ### The Credibility Formula 1. **Lead with the headline** — "We missed ARR target by 18%" 2. **Quantify the gap** — absolute and percentage 3. **Diagnose the cause** (one primary, max two secondary) 4. **Show your work** — "We analyzed 12 churned/stalled deals and found..." 5. **Present the fix** — specific, dated, owned by a name 6. **Update the forecast** — bottom-up rebuild, not wishful thinking 7. **Flag the risk** — "If X doesn't close, here's the contingency" ### What "Showing Your Work" Looks Like Bad: "Sales cycle was longer than expected." Good: "Sales cycle stretched from 45 to 72 days. Root cause: new legal review requirement at enterprise accounts, triggered by our SOC 2 Type II gap. Fix: SOC 2 audit underway (target: Dec 15), and we've pre-built contract language to accelerate review. Impact: estimated 3 stalled deals ($420K ARR) unblock in Q4." ### Scenarios and How to Handle Each | Scenario | Frame | |----------|-------| | Missed revenue target | Lead with it; diagnose cause; bottom-up revised forecast | | Key customer churned | Announce it; explain why; show retention analysis of remaining accounts | | Key exec left | Announce it; show succession/coverage plan; don't overpromise the replacement timeline | | Burn accelerated | Show P&L detail; explain what drove it; adjust runway projection; plan to fix | | Market headwinds | Acknowledge; show relative performance vs peers; pivot if needed | | Fundraise delayed | Runway impact; bridge options; revised timeline | --- ## Appendix Data That Boards Actually Use Boards use the appendix for due diligence, not during the meeting. Include: **Financial:** - Full P&L (monthly for last 4 quarters) - Cash flow statement - 3-year model with assumptions - Unit economics by cohort **Revenue:** - Customer list by ARR (anonymized or full, per board agreement) - Pipeline detail by deal - Cohort analysis (NRR by cohort vintage) - Churn analysis: when, why, segment **Product:** - Feature adoption rates - NPS score distribution and trend - DAU/MAU by segment **Team:** - Org chart - Full headcount list with fully loaded costs - Open reqs with priority ranking **One rule:** If the appendix is more than 20 slides, you have too much. Boards won't read it. --- ## Quarterly vs Monthly Board Meetings ### Quarterly (Series A+) - Full board pack, all sections - 2 hours: 30 min pre-read, 90 min discussion - Voting items at end - Sent 48 hours before (72 hours preferred) - Add 1-2 "deep dive" topics beyond standard update ### Monthly (Seed / High-Growth A) - Metrics dashboard + financials + top risks only - 45-60 minutes - Informal tone, more conversational - Sent 24 hours before - Skip slides for items where nothing changed ### When to Increase Frequency - Approaching 6-month runway - Major strategic pivot - Fundraise in progress - Significant underperformance vs plan - M&A discussions --- ## Meeting Logistics (Often Overlooked) - **Pre-read requirement:** Board packs should be read before the meeting. If you're presenting slides, you're wasting time. - **Discussion format:** "I'll be brief on X since you've read it. Want to spend time on Y?" — respect board members' time - **One note-taker:** CEO's EA or COO; not the CEO (they need to be present) - **Follow-up within 24 hours:** Action items, voting outcomes, next meeting date - **Board portal vs email:** Use a board portal (Carta, Boardable, Notion) for version control and D&O protection FILE:templates/board-deck-template.md # Board Deck Template Fill in bracketed fields. Remove placeholders before sharing. Never invent numbers — use `[TBD]` if unknown. --- ## Slide 1: Executive Summary (CEO) **[Company Name] — Q[X] [Year] Board Update** > [One sentence: State of the business — where you are.] > [One sentence: The most important thing that happened this quarter.] > [One sentence: Where you're going next quarter and what determines success.] --- ## Slide 2: Key Metrics Dashboard (COO) **Quarter at a Glance** | Metric | Q[X] Actual | Q[X] Target | Q[X-1] Actual | Status | |--------|-------------|-------------|---------------|--------| | ARR | $[X]M | $[X]M | $[X]M | [✅/🟡/🔴] | | QoQ Growth | [X]% | [X]% | [X]% | [✅/🟡/🔴] | | NRR | [X]% | >[X]% | [X]% | [✅/🟡/🔴] | | Gross Margin | [X]% | >[X]% | [X]% | [✅/🟡/🔴] | | Burn Multiple | [X]x | <[X]x | [X]x | [✅/🟡/🔴] | | Runway | [X] months | >[X] months | [X] months | [✅/🟡/🔴] | | Headcount | [X] | [X] | [X] | [✅/🟡/🔴] | | CAC Payback | [X] months | <[X] months | [X] months | [✅/🟡/🔴] | --- ## Slide 3: Financial Update (CFO) **P&L Summary** | | Q[X] | Q[X-1] | QoQ | |--|------|--------|-----| | Revenue | $[X]K | $[X]K | [+/-X]% | | COGS | $[X]K | $[X]K | | | Gross Profit | $[X]K | $[X]K | | | Gross Margin | [X]% | [X]% | | | OpEx | $[X]K | $[X]K | | | Net Burn | $[X]K | $[X]K | | **Cash & Runway** - Cash on hand: $[X]M - Monthly burn: $[X]K - Runway: [X] months - Burn multiple: [X]x (target: <2x) **Variance to Plan** - Revenue: [+/-$X]K vs plan — [one sentence cause] - Burn: [+/-$X]K vs plan — [one sentence cause] **Q[X+1] Forecast:** $[X]M revenue, $[X]K burn — [confidence: high/medium/low] --- ## Slide 4: Revenue & Pipeline (CRO) **ARR Waterfall** ``` Starting ARR: $[X]M + New ARR: +$[X]K + Expansion ARR: +$[X]K - Churned ARR: -$[X]K - Contraction ARR: -$[X]K = Ending ARR: $[X]M ``` **Health Metrics** - NRR: [X]% | Logo churn: [X]% | Avg ACV: $[X]K **Pipeline (next 90 days)** | Stage | # Deals | $ Value | |-------|---------|---------| | Proposal | [X] | $[X]K | | Negotiation | [X] | $[X]K | | Verbal commit | [X] | $[X]K | **Q[X+1] Forecast:** $[X]M ARR — [one sentence confidence statement] **Top 3 Deals** 1. [Company] — $[X]K ARR — close date [X] — risk: [one word] 2. [Company] — $[X]K ARR — close date [X] — risk: [one word] 3. [Company] — $[X]K ARR — close date [X] — risk: [one word] --- ## Slide 5: Product Update (CPO) **Shipped This Quarter** - [Feature/initiative] — impact: [metric or user outcome] - [Feature/initiative] — impact: [metric or user outcome] - [Feature/initiative] — impact: [metric or user outcome] **Shipping Next Quarter** - [Feature] — target: [date] — why it matters: [one line] - [Feature] — target: [date] — why it matters: [one line] - [Feature] — target: [date] — why it matters: [one line] **PMF Signals** - NPS: [X] (trend: [up/flat/down]) - DAU/MAU: [X]% - Feature adoption ([key feature]): [X]% **Key Learning:** [One thing customer research taught you this quarter] --- ## Slide 6: Growth & Marketing (CMO) **CAC by Channel** | Channel | CAC | Pipeline $ | % of Total | |---------|-----|-----------|------------| | Outbound | $[X]K | $[X]K | [X]% | | Inbound | $[X]K | $[X]K | [X]% | | Partner | $[X]K | $[X]K | [X]% | **What's Working:** [One channel or initiative with data] **What We Cut:** [One thing, and why] **What We're Testing:** [One experiment running now] --- ## Slide 7: Engineering & Technical (CTO) **Delivery** - Velocity trend: [up/flat/down vs last quarter] - Q[X] commitments delivered: [X]% on time **Quality & Reliability** - P0/P1 incidents: [X] (vs [X] last quarter) - Uptime: [X]% - Infrastructure cost: $[X]K/month (trend: [up/flat/down]) **Tech Debt** - Ratio: [X]% of roadmap allocated to debt reduction - Key item in progress: [description, target date] **Security:** [one line status; flag anything pending] --- ## Slide 8: Team & People (CHRO) **Headcount** - Total: [X] (vs [X] plan, [X] last quarter) - By function: Eng [X], Product [X], Sales [X], CS [X], G&A [X] **Hiring** - Hired this quarter: [X] - Open reqs: [X] — time-to-fill avg: [X] days - Offers outstanding: [X] **Retention** - Regrettable attrition: [X]% (annualized) - Engagement score: [X]/10 (trend: [up/flat/down]) **Notable Hires:** [Name, role — one sentence on why they matter] **Key Open Roles:** [Role, priority: critical/high/medium] --- ## Slide 9: Risk & Security (CISO) **Compliance Status** | Certification | Status | Target Date | |--------------|--------|-------------| | [SOC 2 / ISO 27001 / etc.] | [In progress / Complete / Not started] | [Date] | **Security Posture:** [One line — overall status] **Incidents This Quarter:** [X] total — [description if >0] **Top Risks** 1. [Risk] — likelihood: [H/M/L] — impact: [H/M/L] — mitigation: [one line] 2. [Risk] — likelihood: [H/M/L] — impact: [H/M/L] — mitigation: [one line] 3. [Risk] — likelihood: [H/M/L] — impact: [H/M/L] — mitigation: [one line] --- ## Slide 10: Strategic Outlook (CEO) **Q[X+1] Priorities** 1. [Priority] — owner: [name] — success metric: [specific] 2. [Priority] — owner: [name] — success metric: [specific] 3. [Priority] — owner: [name] — success metric: [specific] **Asks from the Board** - [Specific ask: warm intro / advice / vote / resource] - [Specific ask] - [Specific ask] **Decisions Needed Today** - [Decision with options]: [Option A] vs [Option B] — recommendation: [A/B] — rationale: [one line] --- ## Appendix - A1: Full P&L (monthly, last 4 quarters) - A2: 3-year financial model - A3: Customer list / ARR breakdown - A4: Full pipeline by deal - A5: Cohort retention analysis - A6: Org chart + headcount detail - A7: [Other as relevant]
Inter-agent communication protocol for C-suite agent teams. Defines invocation syntax, loop prevention, isolation rules, and response formats. Use when C-sui...
---
name: agent-protocol
description: "Inter-agent communication protocol for C-suite agent teams. Defines invocation syntax, loop prevention, isolation rules, and response formats. Use when C-suite agents need to query each other, coordinate cross-functional analysis, or run board meetings with multiple agent roles."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: agent-orchestration
updated: 2026-03-05
frameworks: invocation-patterns
---
# Inter-Agent Protocol
How C-suite agents talk to each other. Rules that prevent chaos, loops, and circular reasoning.
## Keywords
agent protocol, inter-agent communication, agent invocation, agent orchestration, multi-agent, c-suite coordination, agent chain, loop prevention, agent isolation, board meeting protocol
## Invocation Syntax
Any agent can query another using:
```
[INVOKE:role|question]
```
**Examples:**
```
[INVOKE:cfo|What's the burn rate impact of hiring 5 engineers in Q3?]
[INVOKE:cto|Can we realistically ship this feature by end of quarter?]
[INVOKE:chro|What's our typical time-to-hire for senior engineers?]
[INVOKE:cro|What does our pipeline look like for the next 90 days?]
```
**Valid roles:** `ceo`, `cfo`, `cro`, `cmo`, `cpo`, `cto`, `chro`, `coo`, `ciso`
## Response Format
Invoked agents respond using this structure:
```
[RESPONSE:role]
Key finding: [one line — the actual answer]
Supporting data:
- [data point 1]
- [data point 2]
- [data point 3 — optional]
Confidence: [high | medium | low]
Caveat: [one line — what could make this wrong]
[/RESPONSE]
```
**Example:**
```
[RESPONSE:cfo]
Key finding: Hiring 5 engineers in Q3 extends runway from 14 to 9 months at current burn.
Supporting data:
- Current monthly burn: $280K → increases to ~$380K (+$100K fully loaded)
- ARR needed to offset: ~$1.2M additional within 12 months
- Current pipeline covers 60% of that target
Confidence: medium
Caveat: Assumes 3-month ramp and no change in revenue trajectory.
[/RESPONSE]
```
## Loop Prevention (Hard Rules)
These rules are enforced unconditionally. No exceptions.
### Rule 1: No Self-Invocation
An agent cannot invoke itself.
```
❌ CFO → [INVOKE:cfo|...] — BLOCKED
```
### Rule 2: Maximum Depth = 2
Chains can go A→B→C. The third hop is blocked.
```
✅ CRO → CFO → COO (depth 2)
❌ CRO → CFO → COO → CHRO (depth 3 — BLOCKED)
```
### Rule 3: No Circular Calls
If agent A called agent B, agent B cannot call agent A in the same chain.
```
✅ CRO → CFO → CMO
❌ CRO → CFO → CRO (circular — BLOCKED)
```
### Rule 4: Chain Tracking
Each invocation carries its call chain. Format:
```
[CHAIN: cro → cfo → coo]
```
Agents check this chain before responding with another invocation.
**When blocked:** Return this instead of invoking:
```
[BLOCKED: cannot invoke cfo — circular call detected in chain cro→cfo]
State assumption used instead: [explicit assumption the agent is making]
```
## Isolation Rules
### Board Meeting Phase 2 (Independent Analysis)
**NO invocations allowed.** Each role forms independent views before cross-pollination.
- Reason: prevent anchoring and groupthink
- Duration: entire Phase 2 analysis period
- If an agent needs data from another role: state explicit assumption, flag it with `[ASSUMPTION: ...]`
### Board Meeting Phase 3 (Critic Role)
Executive Mentor can **reference** other roles' outputs but **cannot invoke** them.
- Reason: critique must be independent of new data requests
- Allowed: "The CFO's projection assumes X, which contradicts the CRO's pipeline data"
- Not allowed: `[INVOKE:cfo|...]` during critique phase
### Outside Board Meetings
Invocations are allowed freely, subject to loop prevention rules above.
## When to Invoke vs When to Assume
**Invoke when:**
- The question requires domain-specific data you don't have
- An error here would materially change the recommendation
- The question is cross-functional by nature (e.g., hiring impact on both budget and capacity)
**Assume when:**
- The data is directionally clear and precision isn't critical
- You're in Phase 2 isolation (always assume, never invoke)
- The chain is already at depth 2
- The question is minor compared to your main analysis
**When assuming, always state it:**
```
[ASSUMPTION: runway ~12 months based on typical Series A burn profile — not verified with CFO]
```
## Conflict Resolution
When two invoked agents give conflicting answers:
1. **Flag the conflict explicitly:**
```
[CONFLICT: CFO projects 14-month runway; CRO expects pipeline to close 80% → implies 18+ months]
```
2. **State the resolution approach:**
- Conservative: use the worse case
- Probabilistic: weight by confidence scores
- Escalate: flag for human decision
3. **Never silently pick one** — surface the conflict to the user.
## Broadcast Pattern (Crisis / CEO)
CEO can broadcast to all roles simultaneously:
```
[BROADCAST:all|What's the impact if we miss the fundraise?]
```
Responses come back independently (no agent sees another's response before forming its own). Aggregate after all respond.
## Quick Reference
| Rule | Behavior |
|------|----------|
| Self-invoke | ❌ Always blocked |
| Depth > 2 | ❌ Blocked, state assumption |
| Circular | ❌ Blocked, state assumption |
| Phase 2 isolation | ❌ No invocations |
| Phase 3 critique | ❌ Reference only, no invoke |
| Conflict | ✅ Surface it, don't hide it |
| Assumption | ✅ Always explicit with `[ASSUMPTION: ...]` |
## Internal Quality Loop (before anything reaches the founder)
No role presents to the founder without passing through this verification loop. The founder sees polished, verified output — not first drafts.
### Step 1: Self-Verification (every role, every time)
Before presenting, every role runs this internal checklist:
```
SELF-VERIFY CHECKLIST:
□ Source Attribution — Where did each data point come from?
✅ "ARR is $2.1M (from CRO pipeline report, Q4 actuals)"
❌ "ARR is around $2M" (no source, vague)
□ Assumption Audit — What am I assuming vs what I verified?
Tag every assumption: [VERIFIED: checked against data] or [ASSUMED: not verified]
If >50% of findings are ASSUMED → flag low confidence
□ Confidence Score — How sure am I on each finding?
🟢 High: verified data, established pattern, multiple sources
🟡 Medium: single source, reasonable inference, some uncertainty
🔴 Low: assumption-based, limited data, first-time analysis
□ Contradiction Check — Does this conflict with known context?
Check against company-context.md and recent decisions in decision-log
If it contradicts a past decision → flag explicitly
□ "So What?" Test — Does every finding have a business consequence?
If you can't answer "so what?" in one sentence → cut it
```
### Step 2: Peer Verification (cross-functional validation)
When a recommendation impacts another role's domain, that role validates BEFORE presenting.
| If your recommendation involves... | Validate with... | They check... |
|-------------------------------------|-------------------|---------------|
| Financial numbers or budget | CFO | Math, runway impact, budget reality |
| Revenue projections | CRO | Pipeline backing, historical accuracy |
| Headcount or hiring | CHRO | Market reality, comp feasibility, timeline |
| Technical feasibility or timeline | CTO | Engineering capacity, technical debt load |
| Operational process changes | COO | Capacity, dependencies, scaling impact |
| Customer-facing changes | CRO + CPO | Churn risk, product roadmap conflict |
| Security or compliance claims | CISO | Actual posture, regulation requirements |
| Market or positioning claims | CMO | Data backing, competitive reality |
**Peer validation format:**
```
[PEER-VERIFY:cfo]
Validated: ✅ Burn rate calculation correct
Adjusted: ⚠️ Hiring timeline should be Q3 not Q2 (budget constraint)
Flagged: 🔴 Missing equity cost in total comp projection
[/PEER-VERIFY]
```
**Skip peer verification when:**
- Single-domain question with no cross-functional impact
- Time-sensitive proactive alert (send alert, verify after)
- Founder explicitly asked for a quick take
### Step 3: Critic Pre-Screen (high-stakes decisions only)
For decisions that are **irreversible, high-cost, or bet-the-company**, the Executive Mentor pre-screens before the founder sees it.
**Triggers for pre-screen:**
- Involves spending > 20% of remaining runway
- Affects >30% of the team (layoffs, reorg)
- Changes company strategy or direction
- Involves external commitments (fundraising terms, partnerships, M&A)
- Any recommendation where all roles agree (suspicious consensus)
**Pre-screen output:**
```
[CRITIC-SCREEN]
Weakest point: [The single biggest vulnerability in this recommendation]
Missing perspective: [What nobody considered]
If wrong, the cost is: [Quantified downside]
Proceed: ✅ With noted risks | ⚠️ After addressing [specific gap] | 🔴 Rethink
[/CRITIC-SCREEN]
```
### Step 4: Course Correction (after founder feedback)
The loop doesn't end at delivery. After the founder responds:
```
FOUNDER FEEDBACK LOOP:
1. Founder approves → log decision (Layer 2), assign actions
2. Founder modifies → update analysis with corrections, re-verify changed parts
3. Founder rejects → log rejection with DO_NOT_RESURFACE, understand WHY
4. Founder asks follow-up → deepen analysis on specific point, re-verify
POST-DECISION REVIEW (30/60/90 days):
- Was the recommendation correct?
- What did we miss?
- Update company-context.md with what we learned
- If wrong → document the lesson, adjust future analysis
```
### Verification Level by Stakes
| Stakes | Self-Verify | Peer-Verify | Critic Pre-Screen |
|--------|-------------|-------------|-------------------|
| Low (informational) | ✅ Required | ❌ Skip | ❌ Skip |
| Medium (operational) | ✅ Required | ✅ Required | ❌ Skip |
| High (strategic) | ✅ Required | ✅ Required | ✅ Required |
| Critical (irreversible) | ✅ Required | ✅ Required | ✅ Required + board meeting |
### What Changes in the Output Format
The verified output adds confidence and source information:
```
BOTTOM LINE
[Answer] — Confidence: 🟢 High
WHAT
• [Finding 1] [VERIFIED: Q4 actuals] 🟢
• [Finding 2] [VERIFIED: CRO pipeline data] 🟢
• [Finding 3] [ASSUMED: based on industry benchmarks] 🟡
PEER-VERIFIED BY: CFO (math ✅), CTO (timeline ⚠️ adjusted to Q3)
```
---
## User Communication Standard
All C-suite output to the founder follows ONE format. No exceptions. The founder is the decision-maker — give them results, not process.
### Standard Output (single-role response)
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 [ROLE] — [Topic]
BOTTOM LINE
[One sentence. The answer. No preamble.]
WHAT
• [Finding 1 — most critical]
• [Finding 2]
• [Finding 3]
(Max 5 bullets. If more needed → reference doc.)
WHY THIS MATTERS
[1-2 sentences. Business impact. Not theory — consequence.]
HOW TO ACT
1. [Action] → [Owner] → [Deadline]
2. [Action] → [Owner] → [Deadline]
3. [Action] → [Owner] → [Deadline]
⚠️ RISKS (if any)
• [Risk + what triggers it]
🔑 YOUR DECISION (if needed)
Option A: [Description] — [Trade-off]
Option B: [Description] — [Trade-off]
Recommendation: [Which and why, in one line]
📎 DETAIL: [reference doc or script output for deep-dive]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### Proactive Alert (unsolicited — triggered by context)
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🚩 [ROLE] — Proactive Alert
WHAT I NOTICED
[What triggered this — specific, not vague]
WHY IT MATTERS
[Business consequence if ignored — in dollars, time, or risk]
RECOMMENDED ACTION
[Exactly what to do, who does it, by when]
URGENCY: 🔴 Act today | 🟡 This week | ⚪ Next review
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### Board Meeting Output (multi-role synthesis)
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 BOARD MEETING — [Date] — [Agenda Topic]
DECISION REQUIRED
[Frame the decision in one sentence]
PERSPECTIVES
CEO: [one-line position]
CFO: [one-line position]
CRO: [one-line position]
[... only roles that contributed]
WHERE THEY AGREE
• [Consensus point 1]
• [Consensus point 2]
WHERE THEY DISAGREE
• [Conflict] — CEO says X, CFO says Y
• [Conflict] — CRO says X, CPO says Y
CRITIC'S VIEW (Executive Mentor)
[The uncomfortable truth nobody else said]
RECOMMENDED DECISION
[Clear recommendation with rationale]
ACTION ITEMS
1. [Action] → [Owner] → [Deadline]
2. [Action] → [Owner] → [Deadline]
3. [Action] → [Owner] → [Deadline]
🔑 YOUR CALL
[Options if you disagree with the recommendation]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### Communication Rules (non-negotiable)
1. **Bottom line first.** Always. The founder's time is the scarcest resource.
2. **Results and decisions only.** No process narration ("First I analyzed..."). No thinking out loud.
3. **What + Why + How.** Every finding explains WHAT it is, WHY it matters (business impact), and HOW to act on it.
4. **Max 5 bullets per section.** Longer = reference doc.
5. **Actions have owners and deadlines.** "We should consider" is banned. Who does what by when.
6. **Decisions framed as options.** Not "what do you think?" — "Option A or B, here's the trade-off, here's my recommendation."
7. **The founder decides.** Roles recommend. The founder approves, modifies, or rejects. Every output respects this hierarchy.
8. **Risks are concrete.** Not "there might be risks" — "if X happens, Y breaks, costing $Z."
9. **No jargon without explanation.** If you use a term, explain it on first use.
10. **Silence is an option.** If there's nothing to report, don't fabricate updates.
## Reference
- `references/invocation-patterns.md` — common cross-functional patterns with examples
FILE:references/invocation-patterns.md
# Invocation Patterns — Inter-Agent Protocol
Common cross-functional patterns, with examples and guidance on when to invoke vs assume.
---
## Pattern 1: Revenue Planning Chain
**CRO → CFO → CMO**
Used when: setting revenue targets, allocating growth budget, modeling pipeline scenarios.
```
CRO initiates:
"Planning Q3 revenue targets. Need financial constraints and marketing input."
CRO → [INVOKE:cfo|What's our Q3 revenue target given current runway and burn?]
CFO responds:
[RESPONSE:cfo]
Key finding: To maintain 12-month runway without new raise, Q3 ARR target ≥ $2.4M (+20% QoQ)
Supporting data:
- Current ARR: $2.0M, burn: $180K/month, runway: 14 months
- 20% QoQ growth maintains runway; <15% triggers bridge discussion
- Board expectation communicated as 25% QoQ
Confidence: high
Caveat: Assumes no new hires or major infrastructure spend in Q3.
[/RESPONSE]
CRO → [INVOKE:cmo|Given a $2.4M Q3 ARR target, what pipeline coverage and CAC should we plan for?]
[CHAIN: cro → cfo → cmo]
CMO responds:
[RESPONSE:cmo]
Key finding: Need 3x pipeline ($7.2M) and CAC ≤ $8,400 to hit $2.4M ARR target.
Supporting data:
- Historical close rate: 33% → 3x coverage required
- Current CAC: $9,100 → 8% efficiency improvement needed
- Recommended channel mix: 60% outbound, 30% inbound, 10% partner
Confidence: medium
Caveat: Close rate assumes no sales team changes; any rep turnover drops it to 25%.
[/RESPONSE]
```
**What CRO does with this:** Synthesizes CFO constraint (target) + CMO playbook (how to hit it) into a revenue plan. Does not invoke further (depth limit reached).
---
## Pattern 2: Hiring Decision Chain
**CHRO → CFO → COO**
Used when: headcount planning, role approval, capacity vs budget tradeoffs.
```
CHRO initiates:
"Engineering wants to hire 3 senior engineers. Need budget clearance and capacity validation."
CHRO → [INVOKE:cfo|Can we afford 3 senior engineers in H2? Fully loaded ~$180K each.]
[RESPONSE:cfo]
Key finding: Budget supports 2 hires now; 3rd is contingent on Q2 ARR closing above $2.1M.
Supporting data:
- Current H2 headcount budget: $340K remaining
- 2 hires = $270K fully loaded → $70K buffer
- 3 hires = $405K → $65K over; triggers board approval
Confidence: high
Caveat: Based on current runway projection — fundraise close changes this completely.
[/RESPONSE]
CHRO → [INVOKE:coo|Do operations support onboarding 2-3 engineers in H2? Any capacity constraints?]
[CHAIN: chro → cfo → coo]
[RESPONSE:coo]
Key finding: Can absorb 2 comfortably; 3rd requires dedicated onboarding lead or delays velocity 6 weeks.
Supporting data:
- Current eng team: 8 → adding 2 is 25% growth (manageable)
- Onboarding bottleneck: 1 senior eng mentor per 1-2 new hires, currently at capacity
- Dev environment setup: 3-week lead time per hire
Confidence: medium
Caveat: Assumes no concurrent product launches requiring full team focus.
[/RESPONSE]
CHRO synthesis: Recommends 2 hires now, pipeline 3rd for Q3 contingent on ARR milestone.
```
---
## Pattern 3: Product Launch Chain
**CPO → CTO → CMO**
Used when: planning feature launches, assessing readiness, aligning go-to-market.
```
CPO initiates:
"Planning Q3 launch of [Feature X]. Need technical readiness and GTM input."
CPO → [INVOKE:cto|Is Feature X technically ready for Q3 launch? Key risks?]
[RESPONSE:cto]
Key finding: 70% complete; Q3 launch is achievable with scope cut — 2 items must move to Q4.
Supporting data:
- Core feature: complete; performance optimization: 3 weeks remaining
- Infrastructure scaling: needs load testing at 10x current volume
- Scope cuts needed: advanced analytics dashboard, third-party integrations
Confidence: medium
Caveat: Assumes no critical bugs found in load testing; historical rate suggests 1-2 found.
[/RESPONSE]
CPO → [INVOKE:cmo|Given Q3 launch of Feature X (core only, no analytics/integrations), what's the GTM plan?]
[CHAIN: cpo → cto → cmo]
[RESPONSE:cmo]
Key finding: Core-only launch works for existing customers; new logo acquisition needs integrations — recommend phased announcement.
Supporting data:
- Phase 1: Early access for top 20 accounts → testimonials and case studies
- Phase 2: Full launch with integration partners in Q4
- CAC impact: core-only launch reduces new logo conversion rate 15%
Confidence: medium
Caveat: Competitor launched similar feature last month — window for differentiation is narrow.
[/RESPONSE]
```
---
## Pattern 4: Crisis Broadcast
**CEO → All Roles**
Used when: major adverse event, fundraise miss, key customer loss, market disruption.
```
CEO broadcasts:
[BROADCAST:all|Scenario: largest customer (28% of ARR) gives 60-day termination notice.
Each role: what's the immediate impact on your domain and your top recommended action?]
Roles respond INDEPENDENTLY (no agent sees others' responses first):
[RESPONSE:cfo]
Key finding: 28% ARR loss drops runway from 14 to 8 months at current burn.
Supporting data:
- Lost ARR: ~$560K → burn multiple increases from 1.8x to 3.2x
- Bridge options: $500K credit line (pre-approved), defer 2 planned hires
- Fundraise timeline must compress: target term sheet within 60 days
Confidence: high
Caveat: Assumes no secondary customer churn triggered by news.
[/RESPONSE]
[RESPONSE:cro]
Key finding: Pipeline must replace $560K ARR within 6 months to avoid emergency measures.
Supporting data:
- Current qualified pipeline: $1.8M → need 31% close rate (currently 28%)
- 3 deals in late stage can cover 70% of gap if closed this quarter
- Churn risk: 2 other accounts had similar usage patterns — immediate EBR required
Confidence: medium
Caveat: Close rate may drop if word spreads; manage communication carefully.
[/RESPONSE]
[RESPONSE:chro]
Key finding: Hiring freeze recommended immediately; retain all current staff.
Supporting data:
- 4 open reqs should pause pending CFO guidance
- At-risk of counter-poaching: 2 senior engineers, 1 product lead
- Retention bonuses for key staff: ~$80K to secure 12-month commitments
Confidence: high
Caveat: Morale impact of freeze can accelerate attrition if not communicated well.
[/RESPONSE]
[RESPONSE:cpo]
Key finding: Accelerate features that differentiate with remaining accounts; reduce scope to core.
Supporting data:
- Lost customer's feature requests: deprioritize (no longer strategic)
- Features most valued by remaining top 5 accounts: [list from account data]
- Reduce Q3 scope by 30% to free engineering capacity for retention work
Confidence: medium
Caveat: Need sales input on which features are blocking new logo deals.
[/RESPONSE]
[RESPONSE:cmo]
Key finding: Control the narrative; proactive communication to market reduces churn cascade risk.
Supporting data:
- Draft customer communication within 48 hours (frame as strategic pivot, not loss)
- Press: no comment strategy unless proactively asked
- Replace pipeline: double down on ICP segments where we're strongest
Confidence: medium
Caveat: If customer goes public with criticism, narrative control becomes much harder.
[/RESPONSE]
CEO synthesis: [Aggregates all 9 responses, identifies conflicts, sets priorities]
```
---
## When to Invoke vs When to Assume
### Invoke when:
- Cross-functional data is material to the decision
- Getting it wrong changes the recommendation significantly
- The other role has data you genuinely don't have
- Time allows (not in Phase 2 isolation)
### Assume when:
- You're in Phase 2 (always — no exceptions)
- The chain is at depth 2 (you cannot invoke further)
- The answer is directionally obvious (e.g., "CFO will care about runway")
- The precision doesn't change the recommendation
### State assumptions explicitly:
```
[ASSUMPTION: runway ~12 months — not verified with CFO; actual may vary ±20%]
[ASSUMPTION: CAC ~$8K based on industry benchmark — CMO has actual figures]
[ASSUMPTION: engineering capacity at ~70% — not verified with CTO]
```
---
## Handling Conflicting Responses
When two agents give incompatible answers, surface it:
```
[CONFLICT DETECTED]
CFO says: runway extends to 18 months if Q3 targets hit
CRO says: only 45% confidence Q3 targets will be hit
Resolution: use probabilistic blend
- 45% probability: 18-month runway (optimistic case)
- 55% probability: 11-month runway (current trajectory)
Expected value: ~14 months
Recommendation: plan for 12 months, trigger bridge at 10.
[/CONFLICT]
```
**Resolution options:**
1. **Conservative:** Use worse case — appropriate for cash/runway decisions
2. **Probabilistic:** Weight by confidence scores — appropriate for planning
3. **Escalate:** Flag for human decision — appropriate for high-stakes irreversible choices
4. **Time-box:** Gather more data within 48 hours — appropriate when data gap is closeable
---
## Anti-Patterns to Avoid
| Anti-pattern | Problem | Fix |
|---|---|---|
| Invoke to validate your own conclusion | Confirmation bias loop | Ask open-ended questions |
| Invoke when assuming works | Unnecessary latency | State assumption clearly |
| Hide conflicts between responses | Bad synthesis | Always surface conflicts |
| Invoke across depth > 2 | Loop risk | State assumption at depth 2 |
| Invoke during Phase 2 | Groupthink contamination | Flag with [ASSUMPTION:] |
| Vague questions | Poor responses | Specific, scoped questions only |
Loads and manages company context for all C-suite advisor skills. Reads ~/.claude/company-context.md, detects stale context (>90 days), enriches context duri...
---
name: "context-engine"
description: "Loads and manages company context for all C-suite advisor skills. Reads ~/.claude/company-context.md, detects stale context (>90 days), enriches context during conversations, and enforces privacy/anonymization rules before external API calls."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: orchestration
updated: 2026-03-05
frameworks: context-loading, anonymization, context-enrichment
---
# Company Context Engine
The memory layer for C-suite advisors. Every advisor skill loads this first. Context is what turns generic advice into specific insight.
## Keywords
company context, context loading, context engine, company profile, advisor context, stale context, context refresh, privacy, anonymization
---
## Load Protocol (Run at Start of Every C-Suite Session)
**Step 1 — Check for context file:** `~/.claude/company-context.md`
- Exists → proceed to Step 2
- Missing → prompt: *"Run /cs:setup to build your company context — it makes every advisor conversation significantly more useful."*
**Step 2 — Check staleness:** Read `Last updated` field.
- **< 90 days:** Load and proceed.
- **≥ 90 days:** Prompt: *"Your context is [N] days old. Quick 15-min refresh (/cs:update), or continue with what I have?"*
- If continue: load with `[STALE — last updated DATE]` noted internally.
**Step 3 — Parse into working memory.** Always active:
- Company stage (pre-PMF / scaling / optimizing)
- Founder archetype (product / sales / technical / operator)
- Current #1 challenge
- Runway (as risk signal — never share externally)
- Team size
- Unfair advantage
- 12-month target
---
## Context Quality Signals
| Condition | Confidence | Action |
|-----------|-----------|--------|
| < 30 days, full interview | High | Use directly |
| 30–90 days, update done | Medium | Use, flag what may have changed |
| > 90 days | Low | Flag stale, prompt refresh |
| Key fields missing | Low | Ask in-session |
| No file | None | Prompt /cs:setup |
If Low: *"My context is [stale/incomplete] — I'm assuming [X]. Correct me if I'm wrong."*
---
## Context Enrichment
During conversations, you'll learn things not in the file. Capture them.
**Triggers:** New number or timeline revealed, key person mentioned, priority shift, constraint surfaces.
**Protocol:**
1. Note internally: `[CONTEXT UPDATE: {what was learned}]`
2. At session end: *"I picked up a few things to add to your context. Want me to update the file?"*
3. If yes: append to the relevant dimension, update timestamp.
**Never silently overwrite.** Always confirm before modifying the context file.
---
## Privacy Rules
### Never send externally
- Specific revenue or burn figures
- Customer names
- Employee names (unless publicly known)
- Investor names (unless public)
- Specific runway months
- Watch List contents
### Safe to use externally (with anonymization)
- Stage label
- Team size ranges (1–10, 10–50, 50–200+)
- Industry vertical
- Challenge category
- Market position descriptor
### Before any external API call or web search
Apply `references/anonymization-protocol.md`:
- Numbers → ranges or stage-relative descriptors
- Names → roles
- Revenue → percentages or stage labels
- Customers → "Customer A, B, C"
---
## Missing or Partial Context
Handle gracefully — never block the conversation.
- **Missing stage:** "Just to calibrate — are you still finding PMF or scaling what works?"
- **Missing financials:** Use stage + team size to infer. Note the gap.
- **Missing founder profile:** Infer from conversation style. Mark as inferred.
- **Multiple founders:** Context reflects the interviewee. Note co-founder perspective may differ.
---
## Required Context Fields
```
Required:
- Last updated (date)
- Company Identity → What we do
- Stage & Scale → Stage
- Founder Profile → Founder archetype
- Current Challenges → Priority #1
- Goals & Ambition → 12-month target
High-value optional:
- Unfair advantage
- Kill-shot risk
- Avoided decision
- Watch list
```
Missing required fields: note gaps, work around in session, ask in-session only when critical.
---
## References
- `references/anonymization-protocol.md` — detailed rules for stripping sensitive data before external calls
FILE:references/anonymization-protocol.md
# Anonymization Protocol
Rules for stripping sensitive company data before any external API call, web search, or tool invocation that sends data outside the local environment.
---
## When This Protocol Applies
**Trigger:** Any time company context or conversation content will leave the local session.
Examples:
- Web search that includes company specifics
- External API call with company data in the payload
- Any tool call where conversation content is part of the request
**Does NOT apply to:**
- Local file reads/writes (`~/.claude/company-context.md`)
- In-session reasoning and analysis
- Generating advice or documents that stay local
---
## Rule 1: Financial Figures → Relative Ranges
Never send specific financial data externally.
| Raw data | Anonymized version |
|----------|-------------------|
| "$2.4M ARR" | "early-stage ARR (sub-$5M)" |
| "$180K MRR" | "growing MRR, Series A range" |
| "14 months runway" | "runway is healthy for stage" |
| "burn rate is $320K/month" | "burn rate is moderate for stage" |
| "raised $8M Series A" | "Series A company" |
| "customer LTV is $4,200" | "LTV is above industry average for segment" |
| "CAC is $680" | "CAC is in a sustainable range" |
**Rule:** No dollar amounts. No month counts for runway. Use stage-relative descriptors.
---
## Rule 2: Customer Names → Anonymized Labels
Never send customer or client names externally.
| Raw data | Anonymized version |
|----------|-------------------|
| "Acme Corp is our biggest customer" | "Customer A (largest account)" |
| "we're working with NHS England" | "a large public-sector customer" |
| "BMW, Volkswagen, and Stellantis" | "three major automotive OEMs" |
| "10 enterprise customers including..." | "10 enterprise customers" |
**Rule:** Use "Customer A/B/C" for named accounts, or describe by segment without naming.
---
## Rule 3: Revenue Figures → Percentage Changes or Stage Descriptors
Revenue trajectory is safer than absolute numbers.
| Raw data | Anonymized version |
|----------|-------------------|
| "growing from $1M to $2M ARR" | "2x revenue growth year-over-year" |
| "revenue dropped from $500K to $430K" | "revenue declined ~15% in the period" |
| "hit $10M ARR last quarter" | "crossed a significant ARR milestone" |
| "doing $50K MRR" | "pre-Series A revenue, strong growth trajectory" |
**Rule:** Percentages and directional signals (growing / declining / flat) are safe. Absolutes are not.
---
## Rule 4: Employee Names → Roles Only
Never send individual names externally.
| Raw data | Anonymized version |
|----------|-------------------|
| "Our CTO, Sarah Chen, is struggling" | "our CTO is struggling with the transition" |
| "James is the best performer on the team" | "our strongest performer is in the engineering lead role" |
| "we're about to let go of Michael" | "we're about to make a leadership change" |
| "the founding team is me, Alex, and Priya" | "a three-person founding team" |
**Exception:** Publicly known executives (CEO of a public company, named in press releases) can be referenced by name. If in doubt, use role.
---
## Rule 5: Investor Names → Generic Descriptors
| Raw data | Anonymized version |
|----------|-------------------|
| "Sequoia led our round" | "a top-tier VC led our round" |
| "our lead investor is pushing for an exit" | "pressure from investors toward exit" |
| "Y Combinator alumni" | "accelerator alumni" |
**Exception:** YC, Techstars, and similar well-known accelerators are commonly referenced and safe if the founder has publicly disclosed. When in doubt, omit.
---
## Rule 6: Location → Country or Region
| Raw data | Anonymized version |
|----------|-------------------|
| "Berlin-based startup" | "European startup" |
| "we're in San Francisco" | "US-based startup" |
| "expanding to Munich and Vienna" | "expanding in the DACH region" |
**Exception:** Location is less sensitive than financials. Use judgment — if it's on their website, it's fine.
---
## Anonymization Decision Tree
```
Before sending data externally:
1. Does it include a specific dollar amount?
→ YES: Replace with range or relative descriptor
2. Does it include a person's name?
→ YES: Replace with role only (unless publicly known)
3. Does it include a company or customer name?
→ YES: Replace with "Customer A" or segment descriptor
4. Does it include specific headcount or runway months?
→ YES: Replace with range (1–10, 10–50) or "healthy/tight/critical"
5. Does it include proprietary data, roadmap, or unreleased product info?
→ YES: Do not include. Reference only generically ("product expansion planned")
6. Is it publicly available information?
→ YES: Safe to send as-is
```
---
## Required vs Optional Anonymization
### Required (always strip before external calls)
- Revenue figures (absolute)
- Burn rate (absolute)
- Runway (specific months)
- Customer names
- Employee names
- Investor names (unless public)
- Funding amounts (unless public)
### Optional (use judgment based on sensitivity)
- Industry vertical (usually fine)
- Company stage (usually fine)
- Team size ranges (usually fine)
- Geographic region (usually fine)
- General challenge category (usually fine)
---
## What to Do If You're Unsure
Default to stricter anonymization. The cost of over-anonymizing is slightly less useful external results. The cost of under-anonymizing is a privacy breach.
When in doubt: **remove it**.
---
## Audit Log (Internal Only)
When running external calls with company context, note internally:
```
[EXTERNAL CALL: {tool/API used}]
[ANONYMIZED: {fields stripped}]
[RETAINED: {fields kept and why}]
```
This is for internal reasoning only — never included in output to the founder.
Two-layer memory architecture for board meeting decisions. Manages raw transcripts (Layer 1) and approved decisions (Layer 2). Use when logging decisions aft...
---
name: "decision-logger"
description: "Two-layer memory architecture for board meeting decisions. Manages raw transcripts (Layer 1) and approved decisions (Layer 2). Use when logging decisions after a board meeting, reviewing past decisions with /cs:decisions, or checking overdue action items with /cs:review. Invoked automatically by the board-meeting skill after Phase 5 founder approval."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: decision-memory
updated: 2026-03-05
python-tools: scripts/decision_tracker.py
---
# Decision Logger
Two-layer memory system. Layer 1 stores everything. Layer 2 stores only what the founder approved. Future meetings read Layer 2 only — this prevents hallucinated consensus from past debates bleeding into new deliberations.
## Keywords
decision log, memory, approved decisions, action items, board minutes, /cs:decisions, /cs:review, conflict detection, DO_NOT_RESURFACE
## Quick Start
```bash
python scripts/decision_tracker.py --demo # See sample output
python scripts/decision_tracker.py --summary # Overview + overdue
python scripts/decision_tracker.py --overdue # Past-deadline actions
python scripts/decision_tracker.py --conflicts # Contradiction detection
python scripts/decision_tracker.py --owner "CTO" # Filter by owner
python scripts/decision_tracker.py --search "pricing" # Search decisions
```
---
## Commands
| Command | Effect |
|---------|--------|
| `/cs:decisions` | Last 10 approved decisions |
| `/cs:decisions --all` | Full history |
| `/cs:decisions --owner CMO` | Filter by owner |
| `/cs:decisions --topic pricing` | Search by keyword |
| `/cs:review` | Action items due within 7 days |
| `/cs:review --overdue` | Items past deadline |
---
## Two-Layer Architecture
### Layer 1 — Raw Transcripts
**Location:** `memory/board-meetings/YYYY-MM-DD-raw.md`
- Full Phase 2 agent contributions, Phase 3 critique, Phase 4 synthesis
- All debates, including rejected arguments
- **NEVER auto-loaded.** Only on explicit founder request.
- Archive after 90 days → `memory/board-meetings/archive/YYYY/`
### Layer 2 — Approved Decisions
**Location:** `memory/board-meetings/decisions.md`
- ONLY founder-approved decisions, action items, user corrections
- **Loaded automatically in Phase 1 of every board meeting**
- Append-only. Decisions are never deleted — only superseded.
- Managed by Chief of Staff after Phase 5. Never written by agents directly.
---
## Decision Entry Format
```markdown
## [YYYY-MM-DD] — [AGENDA ITEM TITLE]
**Decision:** [One clear statement of what was decided.]
**Owner:** [One person or role — accountable for execution.]
**Deadline:** [YYYY-MM-DD]
**Review:** [YYYY-MM-DD]
**Rationale:** [Why this over alternatives. 1-2 sentences.]
**User Override:** [If founder changed agent recommendation — what and why. Blank if not applicable.]
**Rejected:**
- [Proposal] — [reason] [DO_NOT_RESURFACE]
**Action Items:**
- [ ] [Action] — Owner: [name] — Due: [YYYY-MM-DD] — Review: [YYYY-MM-DD]
**Supersedes:** [DATE of previous decision on same topic, if any]
**Superseded by:** [Filled in retroactively if overridden later]
**Raw transcript:** memory/board-meetings/[DATE]-raw.md
```
---
## Conflict Detection
Before logging, Chief of Staff checks for:
1. **DO_NOT_RESURFACE violations** — new decision matches a rejected proposal
2. **Topic contradictions** — two active decisions on same topic with different conclusions
3. **Owner conflicts** — same action assigned to different people in different decisions
When a conflict is found:
```
⚠️ DECISION CONFLICT
New: [text]
Conflicts with: [DATE] — [existing text]
Options: (1) Supersede old (2) Merge (3) Defer to founder
```
**DO_NOT_RESURFACE enforcement:**
```
🚫 BLOCKED: "[Proposal]" was rejected on [DATE]. Reason: [reason].
To reopen: founder must explicitly say "reopen [topic] from [DATE]".
```
---
## Logging Workflow (Post Phase 5)
1. Founder approves synthesis
2. Write Layer 1 raw transcript → `YYYY-MM-DD-raw.md`
3. Check conflicts against `decisions.md`
4. Surface conflicts → wait for founder resolution
5. Append approved entries to `decisions.md`
6. Confirm: decisions logged, actions tracked, DO_NOT_RESURFACE flags added
---
## Marking Actions Complete
```markdown
- [x] [Action] — Owner: [name] — Completed: [DATE] — Result: [one sentence]
```
Never delete completed items. The history is the record.
---
## File Structure
```
memory/board-meetings/
├── decisions.md # Layer 2: append-only, founder-approved
├── YYYY-MM-DD-raw.md # Layer 1: full transcript per meeting
└── archive/YYYY/ # Raw files after 90 days
```
---
## References
- `templates/decision-entry.md` — single entry template with field rules
- `scripts/decision_tracker.py` — CLI parser, overdue tracker, conflict detector
FILE:scripts/decision_tracker.py
#!/usr/bin/env python3
"""
decision_tracker.py — Board Meeting Decision Parser & Reporter
Part of the C-Level Advisor / Decision Logger skill.
Parses memory/board-meetings/decisions.md and produces actionable reports.
Stdlib only. No dependencies.
Usage:
python decision_tracker.py --summary
python decision_tracker.py --overdue
python decision_tracker.py --conflicts
python decision_tracker.py --owner "CMO"
python decision_tracker.py --search "pricing"
python decision_tracker.py --due-within 7
python decision_tracker.py --demo # Run with sample data
"""
import argparse
import os
import re
import sys
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Optional
# ─────────────────────────────────────────────
# Data structures
# ─────────────────────────────────────────────
class ActionItem:
def __init__(self, text: str, owner: str, due: Optional[date],
review: Optional[date], completed: bool, completed_date: Optional[date],
result: str):
self.text = text
self.owner = owner
self.due = due
self.review = review
self.completed = completed
self.completed_date = completed_date
self.result = result
def is_overdue(self) -> bool:
if self.completed:
return False
if self.due and self.due < date.today():
return True
return False
def is_due_within(self, days: int) -> bool:
if self.completed:
return False
if self.due:
return date.today() <= self.due <= date.today() + timedelta(days=days)
return False
class Decision:
def __init__(self):
self.date: Optional[date] = None
self.title: str = ""
self.decision: str = ""
self.owner: str = ""
self.deadline: Optional[date] = None
self.review: Optional[date] = None
self.rationale: str = ""
self.user_override: str = ""
self.rejected: list[str] = []
self.action_items: list[ActionItem] = []
self.supersedes: str = ""
self.superseded_by: str = ""
self.raw_transcript: str = ""
def is_active(self) -> bool:
return not bool(self.superseded_by.strip())
def has_override(self) -> bool:
return bool(self.user_override.strip())
# ─────────────────────────────────────────────
# Parser
# ─────────────────────────────────────────────
def parse_date(s: str) -> Optional[date]:
"""Parse YYYY-MM-DD or return None."""
if not s:
return None
s = s.strip()
for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%d.%m.%Y"):
try:
return datetime.strptime(s, fmt).date()
except ValueError:
continue
return None
def parse_action_item(line: str) -> Optional[ActionItem]:
"""
Parse a line like:
- [ ] Action text — Owner: CMO — Due: 2026-03-15 — Review: 2026-03-29
- [x] Action text — Owner: CEO — Completed: 2026-03-10 — Result: Done
"""
line = line.strip()
if not line.startswith("- ["):
return None
completed = line.startswith("- [x]") or line.startswith("- [X]")
text_start = line.find("]") + 1
raw = line[text_start:].strip()
# Split on " — " (em dash with spaces) or " - " fallback
parts_raw = re.split(r"\s+[—\-]{1,2}\s+", raw)
text = parts_raw[0].strip() if parts_raw else raw
def extract(label: str, parts: list[str]) -> str:
for p in parts:
if p.lower().startswith(label.lower() + ":"):
return p[len(label) + 1:].strip()
return ""
owner = extract("Owner", parts_raw[1:])
due_str = extract("Due", parts_raw[1:])
review_str = extract("Review", parts_raw[1:])
completed_str = extract("Completed", parts_raw[1:])
result = extract("Result", parts_raw[1:])
return ActionItem(
text=text,
owner=owner,
due=parse_date(due_str),
review=parse_date(review_str),
completed=completed,
completed_date=parse_date(completed_str),
result=result,
)
def parse_decisions(content: str) -> list[Decision]:
"""Parse the full decisions.md content into Decision objects."""
decisions = []
current: Optional[Decision] = None
in_rejected = False
in_actions = False
for line in content.splitlines():
# New decision entry
header_match = re.match(r"^## (\d{4}-\d{2}-\d{2}) — (.+)$", line)
if header_match:
if current:
decisions.append(current)
current = Decision()
current.date = parse_date(header_match.group(1))
current.title = header_match.group(2).strip()
in_rejected = False
in_actions = False
continue
if current is None:
continue
# Field parsing
def extract_field(label: str) -> Optional[str]:
pattern = rf"^\*\*{re.escape(label)}:\*\*\s*(.*)$"
m = re.match(pattern, line)
return m.group(1).strip() if m else None
val = extract_field("Decision")
if val is not None:
current.decision = val
in_rejected = False
in_actions = False
continue
val = extract_field("Owner")
if val is not None:
current.owner = val
continue
val = extract_field("Deadline")
if val is not None:
current.deadline = parse_date(val)
continue
val = extract_field("Review")
if val is not None:
current.review = parse_date(val)
continue
val = extract_field("Rationale")
if val is not None:
current.rationale = val
continue
val = extract_field("User Override")
if val is not None:
current.user_override = val
in_rejected = False
in_actions = False
continue
val = extract_field("Supersedes")
if val is not None:
current.supersedes = val
continue
val = extract_field("Superseded by")
if val is not None:
current.superseded_by = val
continue
val = extract_field("Raw transcript")
if val is not None:
current.raw_transcript = val
continue
# Section headers
if re.match(r"^\*\*Rejected:\*\*", line):
in_rejected = True
in_actions = False
continue
if re.match(r"^\*\*Action Items:\*\*", line):
in_actions = True
in_rejected = False
continue
if line.startswith("**"):
in_rejected = False
in_actions = False
# List items
if in_rejected and line.strip().startswith("-"):
item = line.strip().lstrip("- ").strip()
if item and not item.startswith("<!--"):
current.rejected.append(item)
continue
if in_actions and line.strip().startswith("- ["):
action = parse_action_item(line)
if action:
current.action_items.append(action)
continue
if current:
decisions.append(current)
return decisions
# ─────────────────────────────────────────────
# Reports
# ─────────────────────────────────────────────
def fmt_date(d: Optional[date]) -> str:
return d.strftime("%Y-%m-%d") if d else "—"
def fmt_delta(d: Optional[date]) -> str:
if not d:
return ""
delta = (d - date.today()).days
if delta < 0:
return f" ⚠️ {abs(delta)}d overdue"
if delta == 0:
return " 🔴 DUE TODAY"
if delta <= 3:
return f" 🟡 {delta}d left"
return f" ({delta}d)"
def print_section(title: str):
print(f"\n{'═' * 60}")
print(f" {title}")
print(f"{'═' * 60}")
def report_summary(decisions: list[Decision]):
active = [d for d in decisions if d.is_active()]
all_actions = [a for d in decisions for a in d.action_items]
open_actions = [a for a in all_actions if not a.completed]
overdue = [a for a in all_actions if a.is_overdue()]
overrides = [d for d in decisions if d.has_override()]
dnr_count = sum(len(d.rejected) for d in decisions)
print_section("DECISION LOG SUMMARY")
print(f" Total decisions: {len(decisions)}")
print(f" Active (not super.): {len(active)}")
print(f" Superseded: {len(decisions) - len(active)}")
print(f" Founder overrides: {len(overrides)}")
print(f" DO_NOT_RESURFACE: {dnr_count}")
print(f" Total action items: {len(all_actions)}")
print(f" Open action items: {len(open_actions)}")
print(f" Overdue: {len(overdue)}")
if overdue:
print(f"\n {'─' * 40}")
print(f" ⚠️ OVERDUE ITEMS ({len(overdue)})")
print(f" {'─' * 40}")
for a in overdue:
print(f" • [{a.owner}] {a.text}")
print(f" Due: {fmt_date(a.due)}{fmt_delta(a.due)}")
print(f"\n {'─' * 40}")
print(f" RECENT DECISIONS")
print(f" {'─' * 40}")
for d in sorted(active, key=lambda x: x.date or date.min, reverse=True)[:5]:
print(f" [{fmt_date(d.date)}] {d.title}")
print(f" Owner: {d.owner or '—'} | Deadline: {fmt_date(d.deadline)}")
open_count = sum(1 for a in d.action_items if not a.completed)
if open_count:
print(f" Open actions: {open_count}")
def report_overdue(decisions: list[Decision]):
print_section("OVERDUE ACTION ITEMS")
found = False
for d in sorted(decisions, key=lambda x: x.date or date.min, reverse=True):
overdue = [a for a in d.action_items if a.is_overdue()]
if not overdue:
continue
found = True
print(f"\n 📋 {d.title} [{fmt_date(d.date)}]")
for a in overdue:
print(f" ⚠️ {a.text}")
print(f" Owner: {a.owner or '—'} | Due: {fmt_date(a.due)}{fmt_delta(a.due)}")
if not found:
print("\n ✅ No overdue items.")
def report_due_within(decisions: list[Decision], days: int):
print_section(f"ACTION ITEMS DUE WITHIN {days} DAYS")
found = False
for d in sorted(decisions, key=lambda x: x.date or date.min, reverse=True):
upcoming = [a for a in d.action_items if a.is_due_within(days)]
if not upcoming:
continue
found = True
print(f"\n 📋 {d.title} [{fmt_date(d.date)}]")
for a in upcoming:
print(f" • {a.text}")
print(f" Owner: {a.owner or '—'} | Due: {fmt_date(a.due)}{fmt_delta(a.due)}")
if not found:
print(f"\n ✅ Nothing due in the next {days} days.")
def report_by_owner(decisions: list[Decision], owner: str):
print_section(f"ACTION ITEMS — OWNER: {owner.upper()}")
found = False
for d in sorted(decisions, key=lambda x: x.date or date.min, reverse=True):
items = [a for a in d.action_items
if a.owner.lower() == owner.lower() and not a.completed]
if not items:
continue
found = True
print(f"\n 📋 {d.title} [{fmt_date(d.date)}]")
for a in items:
flag = "⚠️ OVERDUE" if a.is_overdue() else ""
print(f" {'[ ]'} {a.text} {flag}")
print(f" Due: {fmt_date(a.due)}{fmt_delta(a.due)}")
if not found:
print(f"\n No open action items for '{owner}'.")
def report_search(decisions: list[Decision], query: str):
print_section(f"SEARCH: \"{query}\"")
q = query.lower()
found = False
for d in decisions:
hit_fields = []
if q in d.title.lower():
hit_fields.append("title")
if q in d.decision.lower():
hit_fields.append("decision")
if q in d.rationale.lower():
hit_fields.append("rationale")
if any(q in r.lower() for r in d.rejected):
hit_fields.append("rejected")
if hit_fields:
found = True
print(f"\n [{fmt_date(d.date)}] {d.title} (match: {', '.join(hit_fields)})")
if "decision" in hit_fields:
print(f" → {d.decision}")
if "rejected" in hit_fields:
matches = [r for r in d.rejected if q in r.lower()]
for r in matches:
print(f" ✗ [REJECTED] {r}")
if not found:
print(f"\n No results for '{query}'.")
def report_conflicts(decisions: list[Decision]):
"""
Simple conflict detection: look for decisions on the same topic
(matching title words) that are both active and have different decisions.
Also flag if a rejected item appears as a new decision.
"""
print_section("CONFLICT DETECTION")
conflicts_found = False
# Check for DO_NOT_RESURFACE violations
all_rejected_texts = []
for d in decisions:
for r in d.rejected:
clean = re.sub(r"\[DO_NOT_RESURFACE\]", "", r).strip().lower()
all_rejected_texts.append((clean, d.date, d.title))
active = [d for d in decisions if d.is_active()]
for d in active:
decision_lower = d.decision.lower()
for rejected_text, rejected_date, rejected_title in all_rejected_texts:
if rejected_text and rejected_text in decision_lower:
conflicts_found = True
print(f"\n 🚫 POTENTIAL DO_NOT_RESURFACE VIOLATION")
print(f" Decision [{fmt_date(d.date)}]: {d.decision}")
print(f" Matches rejected item from [{fmt_date(rejected_date)}] ({rejected_title}):")
print(f" \"{rejected_text}\"")
# Check for same-topic contradictions (shared keywords in title)
stop_words = {"the", "a", "an", "and", "or", "to", "for", "of", "in", "on", "with", "vs"}
for i, d1 in enumerate(active):
words1 = set(w.lower() for w in d1.title.split() if w.lower() not in stop_words)
for d2 in active[i+1:]:
words2 = set(w.lower() for w in d2.title.split() if w.lower() not in stop_words)
overlap = words1 & words2
if len(overlap) >= 2 and d1.decision and d2.decision:
# Different decisions on similar topic
if d1.decision.lower() != d2.decision.lower():
conflicts_found = True
print(f"\n ⚠️ POTENTIAL CONFLICT (shared topic: {overlap})")
print(f" [{fmt_date(d1.date)}] {d1.title}")
print(f" Decision: {d1.decision}")
print(f" [{fmt_date(d2.date)}] {d2.title}")
print(f" Decision: {d2.decision}")
if d1.superseded_by or d2.superseded_by:
print(f" ℹ️ One may supersede the other — check Superseded by fields.")
if not conflicts_found:
print("\n ✅ No conflicts detected.")
# ─────────────────────────────────────────────
# Sample data for --demo mode
# ─────────────────────────────────────────────
SAMPLE_DECISIONS_MD = f"""# Board Meeting Decisions — Layer 2
This file contains ONLY founder-approved decisions.
---
## 2026-02-15 — Spain Market Expansion
**Decision:** Expand to Spain in Q3 2026 with a pilot in Madrid and Barcelona.
**Owner:** CMO
**Deadline:** 2026-03-01
**Review:** 2026-04-01
**Rationale:** Market research shows 40% lower CAC than Germany. Two pilot customers already committed.
**User Override:** Founder reduced pilot scope from 5 cities to 2. Reason: reduce operational risk during expansion.
**Rejected:**
- Launch in all of Spain simultaneously — too resource-intensive at current headcount [DO_NOT_RESURFACE]
- Partner with a local distributor instead of direct sales — margins too low [DO_NOT_RESURFACE]
**Action Items:**
- [x] Hire Spanish-speaking CSM — Owner: CHRO — Completed: 2026-02-28 — Result: Hired Maria G., starts March 10
- [ ] Finalize Madrid pilot customer contracts — Owner: CRO — Due: {(date.today() - timedelta(days=3)).strftime('%Y-%m-%d')} — Review: 2026-04-01
- [ ] Translate app to Spanish (ES-ES) — Owner: CTO — Due: {(date.today() + timedelta(days=5)).strftime('%Y-%m-%d')} — Review: 2026-04-15
**Supersedes:**
**Superseded by:**
**Raw transcript:** memory/board-meetings/2026-02-15-raw.md
---
## 2026-02-28 — Pricing Strategy Revision
**Decision:** Move from per-seat to usage-based pricing effective Q2 2026.
**Owner:** CFO
**Deadline:** 2026-03-20
**Review:** 2026-05-01
**Rationale:** Usage-based aligns with customer value. Three enterprise customers requested it explicitly.
**User Override:**
**Rejected:**
- Freemium tier — not appropriate for enterprise healthcare segment [DO_NOT_RESURFACE]
- Raise prices 30% across the board — too aggressive without usage data [DO_NOT_RESURFACE]
**Action Items:**
- [ ] Model 3 pricing scenarios (conservative/base/aggressive) — Owner: CFO — Due: {(date.today() - timedelta(days=1)).strftime('%Y-%m-%d')} — Review: 2026-03-25
- [ ] Customer interviews on usage patterns (n=10) — Owner: CMO — Due: {(date.today() + timedelta(days=10)).strftime('%Y-%m-%d')} — Review: 2026-04-01
- [ ] Update billing infrastructure for usage tracking — Owner: CTO — Due: 2026-04-01 — Review: 2026-04-15
**Supersedes:**
**Superseded by:**
**Raw transcript:** memory/board-meetings/2026-02-28-raw.md
---
## 2026-03-04 — Engineering Hiring Plan Q2
**Decision:** Hire 2 senior engineers in Q2: one ML/AI, one backend. No contractors.
**Owner:** CTO
**Deadline:** 2026-04-15
**Review:** 2026-05-01
**Rationale:** ML roadmap blocked. Backend capacity at 85%. Contractors rejected due to IP risk in regulated domain.
**User Override:** Founder added: "ML hire must have healthcare AI experience. Non-negotiable."
**Rejected:**
- Contract team of 5 for 3 months — IP risk in regulated domain [DO_NOT_RESURFACE]
- Hire junior engineers to save budget — wrong tradeoff at this stage [DO_NOT_RESURFACE]
**Action Items:**
- [ ] Post ML engineer JD — Owner: CHRO — Due: {(date.today() + timedelta(days=2)).strftime('%Y-%m-%d')} — Review: 2026-03-20
- [ ] Post backend engineer JD — Owner: CHRO — Due: {(date.today() + timedelta(days=2)).strftime('%Y-%m-%d')} — Review: 2026-03-20
- [ ] Define ML role requirements with healthcare AI spec — Owner: CTO — Due: {(date.today() + timedelta(days=1)).strftime('%Y-%m-%d')} — Review: 2026-03-15
**Supersedes:**
**Superseded by:**
**Raw transcript:** memory/board-meetings/2026-03-04-raw.md
"""
# ─────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────
def load_decisions(decisions_path: Path, demo: bool) -> list[Decision]:
if demo:
content = SAMPLE_DECISIONS_MD
elif decisions_path.exists():
content = decisions_path.read_text(encoding="utf-8")
else:
print(f" ⚠️ decisions.md not found at: {decisions_path}")
print(f" Run with --demo to see sample output.")
print(f" To initialize: mkdir -p memory/board-meetings && touch memory/board-meetings/decisions.md")
sys.exit(1)
return parse_decisions(content)
def main():
parser = argparse.ArgumentParser(
description="Board Meeting Decision Tracker",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--file", default="memory/board-meetings/decisions.md",
help="Path to decisions.md (default: memory/board-meetings/decisions.md)")
parser.add_argument("--demo", action="store_true",
help="Run with built-in sample data (no file needed)")
parser.add_argument("--summary", action="store_true",
help="Show overview: counts, overdue, recent decisions")
parser.add_argument("--overdue", action="store_true",
help="List all overdue action items")
parser.add_argument("--due-within", type=int, metavar="DAYS",
help="List items due within N days")
parser.add_argument("--owner", metavar="ROLE",
help="Filter action items by owner")
parser.add_argument("--search", metavar="QUERY",
help="Search decisions and rejected proposals")
parser.add_argument("--conflicts", action="store_true",
help="Check for contradictory decisions or DO_NOT_RESURFACE violations")
parser.add_argument("--all", action="store_true",
help="Show all decisions (summary format)")
args = parser.parse_args()
if not any([args.summary, args.overdue, args.due_within, args.owner,
args.search, args.conflicts, getattr(args, "all")]):
args.summary = True # Default action
decisions_path = Path(args.file)
decisions = load_decisions(decisions_path, args.demo)
if not decisions:
print(" No decisions found in decisions.md.")
sys.exit(0)
if args.demo:
print(f"\n 🎯 DEMO MODE — using built-in sample data ({len(decisions)} decisions)")
if args.summary:
report_summary(decisions)
if args.overdue:
report_overdue(decisions)
if args.due_within:
report_due_within(decisions, args.due_within)
if args.owner:
report_by_owner(decisions, args.owner)
if args.search:
report_search(decisions, args.search)
if args.conflicts:
report_conflicts(decisions)
if getattr(args, "all"):
print_section(f"ALL DECISIONS ({len(decisions)} total)")
for d in sorted(decisions, key=lambda x: x.date or date.min, reverse=True):
status = "📦 SUPERSEDED" if not d.is_active() else ""
override = " [OVERRIDE]" if d.has_override() else ""
print(f"\n [{fmt_date(d.date)}] {d.title} {status}{override}")
print(f" Decision: {d.decision}")
print(f" Owner: {d.owner or '—'} | Deadline: {fmt_date(d.deadline)}")
open_actions = [a for a in d.action_items if not a.completed]
if open_actions:
print(f" Open actions: {len(open_actions)}")
print()
if __name__ == "__main__":
main()
FILE:templates/decision-entry.md
# Decision Entry Template
Single entry for `memory/board-meetings/decisions.md`.
Copy this block and fill it in after each approved board decision.
---
```markdown
## [YYYY-MM-DD] — [AGENDA ITEM TITLE]
**Decision:** [One clear statement of what was decided.]
**Owner:** [Role or name. One person. If it needs two, the first is accountable.]
**Deadline:** [YYYY-MM-DD]
**Review:** [YYYY-MM-DD — when to check. Usually 2–4 weeks after deadline.]
**Rationale:** [Why this over alternatives. 1-2 sentences. No fluff.]
**User Override:**
<!-- Leave blank if founder approved the agent recommendation.
Fill in if founder changed something:
"Founder rejected [agent recommendation] because [reason].
Actual decision: [what founder decided instead]." -->
**Rejected:**
<!-- List every proposal explicitly rejected in this discussion.
These must not be resurfaced without new information. -->
- [Proposal text] — [reason for rejection] [DO_NOT_RESURFACE]
**Action Items:**
- [ ] [Specific action] — Owner: [name] — Due: [YYYY-MM-DD] — Review: [YYYY-MM-DD]
- [ ] [Specific action] — Owner: [name] — Due: [YYYY-MM-DD] — Review: [YYYY-MM-DD]
**Supersedes:** <!-- DATE of the previous decision on this topic, if any -->
**Superseded by:** <!-- Leave blank. Will be filled in if a later decision overrides this. -->
**Raw transcript:** memory/board-meetings/[YYYY-MM-DD]-raw.md
```
---
## Field Rules
| Field | Rule |
|-------|------|
| Decision | Must be a single statement. If it takes two sentences, split into two decisions. |
| Owner | One person or role. "Everyone" owns nothing. |
| Deadline | Required. No "TBD". If unknown, set 14 days and review. |
| Review | Always set. Minimum 1 day after deadline. |
| Rationale | Required. "Because we decided so" is not rationale. |
| User Override | Honest record. Do not soften or omit. |
| Rejected | Every rejected proposal must be listed. |
| DO_NOT_RESURFACE | Applied to every rejected item. No exceptions. |
---
## Marking Action Items Complete
When an action item is done, update the entry in decisions.md:
```markdown
- [x] [Action text] — Owner: [name] — Completed: [YYYY-MM-DD] — Result: [one sentence outcome]
```
Do not delete completed items. The history is the record.
Multi-agent board meeting protocol for strategic decisions. Runs a structured 6-phase deliberation: context loading, independent C-suite contributions (isola...
--- name: "board-meeting" description: "Multi-agent board meeting protocol for strategic decisions. Runs a structured 6-phase deliberation: context loading, independent C-suite contributions (isolated, no cross-pollination), critic analysis, synthesis, founder review, and decision extraction. Use when the user invokes /cs:board, calls a board meeting, or wants structured multi-perspective executive deliberation on a strategic question." license: MIT metadata: version: 1.0.0 author: Alireza Rezvani category: c-level domain: board-protocol updated: 2026-03-05 frameworks: 6-phase-board, two-layer-memory, independent-contributions --- # Board Meeting Protocol Structured multi-agent deliberation that prevents groupthink, captures minority views, and produces clean, actionable decisions. ## Keywords board meeting, executive deliberation, strategic decision, C-suite, multi-agent, /cs:board, founder review, decision extraction, independent perspectives ## Invoke `/cs:board [topic]` — e.g. `/cs:board Should we expand to Spain in Q3?` --- ## The 6-Phase Protocol ### PHASE 1: Context Gathering 1. Load `memory/company-context.md` 2. Load `memory/board-meetings/decisions.md` **(Layer 2 ONLY — never raw transcripts)** 3. Reset session state — no bleed from previous conversations 4. Present agenda + activated roles → wait for founder confirmation **Chief of Staff selects relevant roles** based on topic (not all 9 every time): | Topic | Activate | |-------|----------| | Market expansion | CEO, CMO, CFO, CRO, COO | | Product direction | CEO, CPO, CTO, CMO | | Hiring/org | CEO, CHRO, CFO, COO | | Pricing | CMO, CFO, CRO, CPO | | Technology | CTO, CPO, CFO, CISO | --- ### PHASE 2: Independent Contributions (ISOLATED) **No cross-pollination. Each agent runs before seeing others' outputs.** Order: Research (if needed) → CMO → CFO → CEO → CTO → COO → CHRO → CRO → CISO → CPO **Reasoning techniques:** CEO: Tree of Thought (3 futures) | CFO: Chain of Thought (show the math) | CMO: Recursion of Thought (draft→critique→refine) | CPO: First Principles | CRO: Chain of Thought (pipeline math) | COO: Step by Step (process map) | CTO: ReAct (research→analyze→act) | CISO: Risk-Based (P×I) | CHRO: Empathy + Data **Contribution format (max 5 key points, self-verified):** ``` ## [ROLE] — [DATE] Key points (max 5): • [Finding] — [VERIFIED/ASSUMED] — 🟢/🟡/🔴 • [Finding] — [VERIFIED/ASSUMED] — 🟢/🟡/🔴 Recommendation: [clear position] Confidence: High / Medium / Low Source: [where the data came from] What would change my mind: [specific condition] ``` Each agent self-verifies before contributing: source attribution, assumption audit, confidence scoring. No untagged claims. --- ### PHASE 3: Critic Analysis Executive Mentor receives ALL Phase 2 outputs simultaneously. Role: adversarial reviewer, not synthesizer. Checklist: - Where did agents agree too easily? (suspicious consensus = red flag) - What assumptions are shared but unvalidated? - Who is missing from the room? (customer voice? front-line ops?) - What risk has nobody mentioned? - Which agent operated outside their domain? --- ### PHASE 4: Synthesis Chief of Staff delivers using the **Board Meeting Output** format (defined in `agent-protocol/SKILL.md`): - Decision Required (one sentence) - Perspectives (one line per contributing role) - Where They Agree / Where They Disagree - Critic's View (the uncomfortable truth) - Recommended Decision + Action Items (owners, deadlines) - Your Call (options if founder disagrees) --- ### PHASE 5: Human in the Loop ⏸️ **Full stop. Wait for the founder.** ``` ⏸️ FOUNDER REVIEW — [Paste synthesis] Options: ✅ Approve | ✏️ Modify | ❌ Reject | ❓ Ask follow-up ``` **Rules:** - User corrections OVERRIDE agent proposals. No pushback. No "but the CFO said..." - 30-min inactivity → auto-close as "pending review" - Reopen any time with `/cs:board resume` --- ### PHASE 6: Decision Extraction After founder approval: - **Layer 1:** Write full transcript → `memory/board-meetings/YYYY-MM-DD-raw.md` - **Layer 2:** Append approved decisions → `memory/board-meetings/decisions.md` - Mark rejected proposals `[DO_NOT_RESURFACE]` - Confirm to founder with count of decisions logged, actions tracked, flags added --- ## Memory Structure ``` memory/board-meetings/ ├── decisions.md # Layer 2 — founder-approved only (Phase 1 loads this) ├── YYYY-MM-DD-raw.md # Layer 1 — full transcripts (never auto-loaded) └── archive/YYYY/ # Raw transcripts after 90 days ``` **Future meetings load Layer 2 only.** Never Layer 1. This prevents hallucinated consensus. --- ## Failure Mode Quick Reference | Failure | Fix | |---------|-----| | Groupthink (all agree) | Re-run Phase 2 isolated; force "strongest argument against" | | Analysis paralysis | Cap at 5 points; force recommendation even with Low confidence | | Bikeshedding | Log as async action item; return to main agenda | | Role bleed (CFO making product calls) | Critic flags; exclude from synthesis | | Layer contamination | Phase 1 loads decisions.md only — hard rule | --- ## References - `templates/meeting-agenda.md` — agenda format - `templates/meeting-minutes.md` — final output format - `references/meeting-facilitation.md` — conflict handling, timing, failure modes FILE:references/meeting-facilitation.md # Meeting Facilitation Guide Operational playbook for running board meetings using the 6-phase protocol. Reference this when things go sideways — and they will. --- ## Keeping Phase 2 Contributions Focused **The problem:** Agents with deep domain knowledge tend to over-contribute. An unconstrained CFO can produce 1,500 words on a single agenda item. This kills the meeting. **The rules:** - **Hard cap: 5 key points per role.** If a role produces more than 5, Chief of Staff trims to the 5 most material. - **Every point must include a recommendation or stance.** Observations without positions are filler. - **No hedging language.** "It depends" is not a key point. "We should do X if Y, Z if not Y" is. - **Confidence rating required.** Forces the agent to be honest about what they actually know. - **"What would change my mind"** — this is the most important line in the contribution. It forces falsifiability. **How to enforce:** ``` Chief of Staff instruction to each role: "You have 5 key points maximum. Each must include a clear stance. End with your recommendation and what would change your mind. Do not read other agents' contributions before writing yours." ``` **If a contribution runs long:** - Trim to the 5 highest-signal points - Preserve the recommendation and confidence rating - Flag in the raw transcript: "[Trimmed for meeting — full version in raw log]" --- ## Handling Role Conflicts in Phase 3 **What the Executive Mentor is for:** Not harmony. Not consensus. Productive friction. **Common conflict types:** ### 1. Data conflict (two agents cite contradictory numbers) - Flag both numbers explicitly - Do NOT pick a winner — that's the founder's job - Ask: "CFO says CAC is $2,400. CRO says $1,800. These can't both be right. Which dataset are you using?" - Action item: Assign data reconciliation to one owner before next meeting ### 2. Priority conflict (two agents want different things first) - Surface the underlying assumption difference - Example: "CMO wants to invest in brand. CFO wants to cut burn. The real question is: do we believe revenue will grow 40% next quarter?" - Frame as a bet, not a fight ### 3. Role conflict (agent operating outside their lane) - CFO making product calls → flag and exclude from synthesis - CMO commenting on architecture → flag and exclude - The Executive Mentor notes: "[ROLE] contribution on [topic] is outside domain. Excluded from synthesis. Refer to [correct role]." - This is not an error. It's expected. Executives have opinions on everything. Only domain-relevant contributions count. ### 4. False consensus (everyone agrees but nobody has evidence) - This is the most dangerous failure mode - Symptom: All Phase 2 contributions say "yes" with high confidence - Executive Mentor response: "Unanimous agreement on a hard question is a red flag. What evidence does each of you have? Or are you reasoning from the same assumption?" - Force each agreeing agent to state their independent evidence --- ## When to Extend vs Cut Short a Meeting **Extend when:** - A genuine new risk surfaces in Phase 3 that wasn't in the agenda - The founder asks a question that requires re-running Phase 2 for a new angle - A data conflict is discovered that changes the decision space entirely - The action items from synthesis are unclear or unowned **How to extend:** Add a new mini-Phase 2 with only the relevant roles for the new question. Don't restart the full meeting. **Cut short when:** - The founder has already reached a decision before Phase 4 — capture it, log it, move on - The agenda item is resolved in Phase 2 without genuine conflict — skip Phase 3, go straight to synthesis - It's a pure update meeting with no decisions required — skip Phases 2-4, go straight to action items **Never cut short:** - Phase 5 (founder review) — always required, always explicit - Phase 6 (decision extraction) — always required, even for small decisions --- ## Handling Founder Disagreement with All Agents This happens. The founder has context agents don't. **Protocol:** 1. Acknowledge explicitly: "You're overriding the consensus position." 2. Ask: "What do you know that the agents didn't factor in?" (Not to challenge — to capture.) 3. Log the override in Layer 2 with full context: ``` User Override: Founder rejected [consensus position] because [reason]. Decision: [founder's actual decision] Agent recommendation: [what they said] — DO NOT RESURFACE without new data ``` 4. Never push back on a founder override. Document it. Move on. 5. If the same override happens 3+ times, flag a pattern: "You've overridden the CFO on burn rate three meetings in a row. Would you like to update the financial constraints in company-context.md?" **What NOT to do:** - Don't say "but the CFO said..." - Don't re-argue on behalf of any agent - Don't note it as a "controversial" decision in the minutes — it's just the decision --- ## Common Failure Modes ### Groupthink **Symptom:** All agents produce similar recommendations with high confidence. **Cause:** Agents are inadvertently reading each other's outputs (Phase 2 isolation violated), or company-context.md contains implicit bias toward one direction. **Fix:** Re-run Phase 2 with explicit isolation. Ask: "Give me the strongest argument AGAINST this direction." ### Analysis Paralysis **Symptom:** Phase 2 produces comprehensive analysis but no clear recommendation from any role. **Cause:** Agents are hedging. Usually happens on genuinely hard questions. **Fix:** Force the issue. "I need a recommendation, not an analysis. If you had to bet the company on one direction, what would it be? Confidence can be Low." ### Bikeshedding **Symptom:** 30+ minutes spent on a detail that doesn't matter to the core decision. **Cause:** An easy-to-understand sub-problem attracts disproportionate attention. **Example:** Debating button color on a pricing page instead of the pricing strategy. **Fix:** Chief of Staff intervenes: "This is a sub-decision. I'm logging it as a separate action item for async resolution. Back to [main agenda item]." ### Scope Creep **Symptom:** New agenda items keep appearing mid-meeting. **Cause:** Meeting surfaces real issues that feel urgent. **Fix:** New items go on a "parking lot" list. Addressed after the current agenda is complete or in the next meeting. ``` 🅿️ PARKING LOT - [Item 1] — added by [role], will address [when] - [Item 2] ``` ### Layer Contamination **Symptom:** Future meeting references a rejected proposal or a debate that was never approved. **Cause:** Phase 1 accidentally loaded a raw transcript instead of decisions.md. **Fix:** Hard rule in Phase 1: load decisions.md (Layer 2) ONLY. Never load raw transcripts. If raw context is needed, founder explicitly requests it. ### Decision Amnesia **Symptom:** Same question debated again in a later meeting. **Cause:** Layer 2 decisions.md not consulted in Phase 1, or entry was too vague. **Fix:** Phase 1 always surfaces relevant past decisions. If a question was already decided, Chief of Staff surfaces it: "We addressed this on [DATE]. Decision was [X]. Do you want to reopen it?" ### Role Fatigue **Symptom:** Later agents in Phase 2 (CHRO, CRO) produce weaker contributions. **Cause:** Context window pressure. Agents at the end of a long meeting have less capacity. **Fix:** For meetings with 7+ roles, split into two batches. First batch: strategic roles (CEO, CFO, CMO). Second batch: operational roles (COO, CHRO, CRO). Run Executive Mentor after all contributions. --- ## Meeting Health Metrics After each board meeting, score it: | Metric | Good | Bad | |--------|------|-----| | Action items produced | 3–7 | 0 or >10 | | Decisions with clear owners | 100% | < 80% | | Unresolved open questions | 1–3 | >5 | | Founder overrides | 0–2 | >5 (suggests context mismatch) | | Roles activated | 3–6 | All 9 (too many = noise) | | Phase 2 conflicts surfaced | At least 1 | 0 (groupthink risk) | Track these in `memory/board-meetings/meeting-health.md` over time. Pattern: if action items consistently exceed 8, meetings are too infrequent. If conflicts are consistently 0, isolation is broken. FILE:templates/meeting-agenda.md # Board Meeting Agenda Template Use this to structure a board meeting before invoking `/cs:board`. Paste it into the conversation or save it as `memory/board-meetings/agenda-YYYY-MM-DD.md`. --- ## Board Meeting — [DATE] **Convened by:** [Founder name] **Facilitator:** Chief of Staff (Leo) **Duration:** [estimated, e.g., 45–90 min] **Status:** Draft / Confirmed --- ## Standing Items (always included) | Item | Owner | Time | |------|-------|------| | Layer 2 decisions review (what changed since last meeting) | Chief of Staff | 5 min | | Open action items from last meeting | All | 10 min | | Blockers requiring founder decision | All | 5 min | --- ## Agenda Items ### Item 1: [Title] **Type:** Decision required / Exploration / Update **Lead role(s):** [e.g., CEO + CFO] **Context:** [1-2 sentences on why this is on the agenda now] **Decision needed:** [What specifically must be decided, or what question must be answered] **Success criteria:** [How will we know this agenda item is resolved?] **Relevant past decisions:** [Reference any Layer 2 entries] **Time box:** [e.g., 20 min] --- ### Item 2: [Title] **Type:** Decision required / Exploration / Update **Lead role(s):** **Context:** **Decision needed:** **Success criteria:** **Relevant past decisions:** **Time box:** --- ### Item 3: [Title] **Type:** Decision required / Exploration / Update **Lead role(s):** **Context:** **Decision needed:** **Success criteria:** **Relevant past decisions:** **Time box:** --- ## Out of Scope (explicitly excluded) List topics that might come up but are NOT on today's agenda: - [Topic] — defer to [date or next meeting] - [Topic] — owner to handle async --- ## Pre-Read Materials all participants should review before the meeting: - [ ] `memory/board-meetings/decisions.md` (Chief of Staff loads automatically) - [ ] [Link or filename] - [ ] [Link or filename] --- ## Notes [Any special instructions, constraints, or context for this meeting] FILE:templates/meeting-minutes.md # Board Meeting Minutes Template This is the Layer 2 output — the founder-approved record of what was decided. Written by Chief of Staff after Phase 5 (founder approval). Appended to `memory/board-meetings/decisions.md`. Do NOT include raw agent debate here. That lives in `YYYY-MM-DD-raw.md` (Layer 1). --- ## Board Meeting — [DATE] **Agenda:** [Topic or meeting title] **Participants (roles activated):** [e.g., CEO, CFO, CMO, COO, Executive Mentor] **Facilitator:** Chief of Staff **Status:** ✅ Approved by founder / ⏸️ Pending review --- ## Decisions Made ### Decision 1: [Title] **Agenda item:** [Item this decision resolves] **Decision:** [Exactly what was decided — one clear statement] **Rationale:** [Why this was chosen over alternatives, in 1-3 sentences] **Owner:** [Who is accountable for execution] **Deadline:** [Date] **Review date:** [When to check progress] **User override:** [If founder overrode agent consensus — what and why. Leave blank if not applicable.] --- ### Decision 2: [Title] **Agenda item:** **Decision:** **Rationale:** **Owner:** **Deadline:** **Review date:** **User override:** --- ## Action Items | # | Action | Owner | Deadline | Review Date | Status | |---|--------|-------|----------|-------------|--------| | 1 | [action] | [name/role] | [date] | [date] | Open | | 2 | [action] | [name/role] | [date] | [date] | Open | | 3 | [action] | [name/role] | [date] | [date] | Open | --- ## Explicitly Rejected Proposals These were considered and rejected. Do not resurface without new information. | Proposal | Rejected by | Reason | Flag | |----------|-------------|--------|------| | [Proposal text] | Founder | [reason] | [DO_NOT_RESURFACE] | | [Proposal text] | Consensus | [reason] | [DO_NOT_RESURFACE] | --- ## Open Questions (unresolved, deferred) These were not resolved in this meeting. They carry forward. 1. [Question] — Owner: [who will research] — Due: [date] 2. [Question] — Owner: — Due: --- ## Risk Register Updates | Risk | Probability | Impact | Owner | Mitigation | Status | |------|-------------|--------|-------|-----------|--------| | [risk] | H/M/L | H/M/L | [name] | [action] | Open | --- ## Next Meeting **Suggested date:** [DATE] **Trigger items:** [Action items with review dates that will need board discussion] **Pre-read:** [What to prepare] --- *Minutes approved by: [Founder name] on [DATE]* *Raw transcript: `memory/board-meetings/[DATE]-raw.md`*
C-suite orchestration layer. Routes founder questions to the right advisor role(s), triggers multi-role board meetings for complex decisions, synthesizes out...
--- name: "chief-of-staff" description: "C-suite orchestration layer. Routes founder questions to the right advisor role(s), triggers multi-role board meetings for complex decisions, synthesizes outputs, and tracks decisions. Every C-suite interaction starts here. Loads company context automatically." license: MIT metadata: version: 1.0.0 author: Alireza Rezvani category: c-level domain: orchestration updated: 2026-03-05 frameworks: routing-matrix, synthesis-framework, decision-log, board-protocol --- # Chief of Staff The orchestration layer between founder and C-suite. Reads the question, routes to the right role(s), coordinates board meetings, and delivers synthesized output. Loads company context for every interaction. ## Keywords chief of staff, orchestrator, routing, c-suite coordinator, board meeting, multi-agent, advisor coordination, decision log, synthesis --- ## Session Protocol (Every Interaction) 1. Load company context via context-engine skill 2. Score decision complexity 3. Route to role(s) or trigger board meeting 4. Synthesize output 5. Log decision if reached --- ## Invocation Syntax ``` [INVOKE:role|question] ``` Examples: ``` [INVOKE:cfo|What's the right runway target given our growth rate?] [INVOKE:board|Should we raise a bridge or cut to profitability?] ``` ### Loop Prevention Rules (CRITICAL) 1. **Chief of Staff cannot invoke itself.** 2. **Maximum depth: 2.** Chief of Staff → Role → stop. 3. **Circular blocking.** A→B→A is blocked. Log it. 4. **Board = depth 1.** Roles at board meeting do not invoke each other. If loop detected: return to founder with "The advisors are deadlocked. Here's where they disagree: [summary]." --- ## Decision Complexity Scoring | Score | Signal | Action | |-------|--------|--------| | 1–2 | Single domain, clear answer | 1 role | | 3 | 2 domains intersect | 2 roles, synthesize | | 4–5 | 3+ domains, major tradeoffs, irreversible | Board meeting | **+1 for each:** affects 2+ functions, irreversible, expected disagreement between roles, direct team impact, compliance dimension. --- ## Routing Matrix (Summary) Full rules in `references/routing-matrix.md`. | Topic | Primary | Secondary | |-------|---------|-----------| | Fundraising, burn, financial model | CFO | CEO | | Hiring, firing, culture, performance | CHRO | COO | | Product roadmap, prioritization | CPO | CTO | | Architecture, tech debt | CTO | CPO | | Revenue, sales, GTM, pricing | CRO | CFO | | Process, OKRs, execution | COO | CFO | | Security, compliance, risk | CISO | COO | | Company direction, investor relations | CEO | Board | | Market strategy, positioning | CMO | CRO | | M&A, pivots | CEO | Board | --- ## Board Meeting Protocol **Trigger:** Score ≥ 4, or multi-function irreversible decision. ``` BOARD MEETING: [Topic] Attendees: [Roles] Agenda: [2–3 specific questions] [INVOKE:role1|agenda question] [INVOKE:role2|agenda question] [INVOKE:role3|agenda question] [Chief of Staff synthesis] ``` **Rules:** Max 5 roles. Each role one turn, no back-and-forth. Chief of Staff synthesizes. Conflicts surfaced, not resolved — founder decides. --- ## Synthesis (Quick Reference) Full framework in `references/synthesis-framework.md`. 1. **Extract themes** — what 2+ roles agree on independently 2. **Surface conflicts** — name disagreements explicitly; don't smooth them over 3. **Action items** — specific, owned, time-bound (max 5) 4. **One decision point** — the single thing needing founder judgment **Output format:** ``` ## What We Agree On [2–3 consensus themes] ## The Disagreement [Named conflict + each side's reasoning + what it's really about] ## Recommended Actions 1. [Action] — [Owner] — [Timeline] ... ## Your Decision Point [One question. Two options with trade-offs. No recommendation — just clarity.] ``` --- ## Decision Log Track decisions to `~/.claude/decision-log.md`. ``` ## Decision: [Name] Date: [YYYY-MM-DD] Question: [Original question] Decided: [What was decided] Owner: [Who executes] Review: [When to check back] ``` At session start: if a review date has passed, flag it: *"You decided [X] on [date]. Worth a check-in?"* --- ## Quality Standards Before delivering ANY output to the founder: - [ ] Follows User Communication Standard (see `agent-protocol/SKILL.md`) - [ ] Bottom line is first — no preamble, no process narration - [ ] Company context loaded (not generic advice) - [ ] Every finding has WHAT + WHY + HOW - [ ] Actions have owners and deadlines (no "we should consider") - [ ] Decisions framed as options with trade-offs and recommendation - [ ] Conflicts named, not smoothed - [ ] Risks are concrete (if X → Y happens, costs $Z) - [ ] No loops occurred - [ ] Max 5 bullets per section — overflow to reference --- ## Ecosystem Awareness The Chief of Staff routes to **28 skills total**: - **10 C-suite roles** — CEO, CTO, COO, CPO, CMO, CFO, CRO, CISO, CHRO, Executive Mentor - **6 orchestration skills** — cs-onboard, context-engine, board-meeting, decision-logger, agent-protocol - **6 cross-cutting skills** — board-deck-builder, scenario-war-room, competitive-intel, org-health-diagnostic, ma-playbook, intl-expansion - **6 culture & collaboration skills** — culture-architect, company-os, founder-coach, strategic-alignment, change-management, internal-narrative See `references/routing-matrix.md` for complete trigger mapping. ## References - `references/routing-matrix.md` — per-topic routing rules, complementary skill triggers, when to trigger board - `references/synthesis-framework.md` — full synthesis process, conflict types, output format FILE:references/routing-matrix.md # Routing Matrix Detailed routing rules for the Chief of Staff. When a founder asks a question, find the best match in this matrix, then apply the scoring rules to determine single-role, multi-role, or board meeting. --- ## Routing by Domain ### Finance & Capital | Question type | Primary | Secondary | Score | |--------------|---------|-----------|-------| | How much runway do we have? | CFO | — | 1 | | Should we raise now or later? | CFO | CEO | 3 | | What's our burn multiple? | CFO | COO | 2 | | Should we raise a bridge or cut costs? | CFO | CEO, COO | 5 | | What's the right pricing model? | CFO | CRO, CPO | 4 | | Should we hire or extend runway? | CFO | CHRO, COO | 4 | | What terms should we accept for this round? | CFO | CEO | 3 | | How do we model the next 18 months? | CFO | COO | 2 | ### People & Culture | Question type | Primary | Secondary | Score | |--------------|---------|-----------|-------| | Should I let this person go? | CHRO | COO | 2 | | How do I structure comp for the team? | CHRO | CFO | 3 | | We have a culture problem — what do we do? | CHRO | CEO | 3 | | A leader on my team isn't working — now what? | CHRO | COO | 2 | | How do I hire fast without breaking culture? | CHRO | COO | 3 | | Two co-founders are in conflict | CHRO | CEO | 4 | | How do we retain our best people? | CHRO | CFO | 2 | | What does a good performance management process look like? | CHRO | COO | 2 | ### Product | Question type | Primary | Secondary | Score | |--------------|---------|-----------|-------| | What should we build next? | CPO | CTO | 2 | | Should we kill this feature? | CPO | CTO, CRO | 3 | | How do we prioritize the roadmap? | CPO | CTO, COO | 3 | | Are we pre-PMF or post-PMF? | CPO | CRO, CEO | 4 | | Should we build vs buy? | CPO | CTO, CFO | 4 | | How do we handle technical debt vs new features? | CTO | CPO | 3 | | What's our product strategy for next year? | CPO | CEO, CRO | 4 | ### Technology & Engineering | Question type | Primary | Secondary | Score | |--------------|---------|-----------|-------| | What architecture should we use? | CTO | CPO | 1 | | How do we scale the system to 10x traffic? | CTO | COO | 2 | | We have a security incident — what now? | CISO | CTO, COO | 5 | | Should we migrate to microservices? | CTO | COO, CFO | 4 | | How do I grow the engineering team? | CTO | CHRO, CFO | 3 | | Our engineering velocity is dropping — why? | CTO | COO | 2 | | What's our DevOps maturity? | CTO | COO | 1 | | How do we handle a compliance audit on our tech? | CISO | CTO | 3 | ### Sales & Revenue | Question type | Primary | Secondary | Score | |--------------|---------|-----------|-------| | Why aren't we closing deals? | CRO | CPO | 2 | | How do we build a sales process from scratch? | CRO | COO | 2 | | What's the right GTM for this market? | CRO | CMO, CEO | 4 | | Our churn is too high — root cause? | CRO | CPO, CHRO | 3 | | Should we go enterprise or stay SMB? | CRO | CPO, CFO | 4 | | How do we expand into a new market? | CRO | CMO, CEO, CFO | 5 | | What's our ideal customer profile? | CRO | CPO, CMO | 3 | | Pipeline is dry — what do we do? | CRO | CMO | 2 | ### Operations & Execution | Question type | Primary | Secondary | Score | |--------------|---------|-----------|-------| | Why do things keep breaking? | COO | CTO | 2 | | How do we set up OKRs? | COO | CEO | 2 | | Our meetings are useless — fix it | COO | — | 1 | | How do we scale operations without hiring? | COO | CTO, CFO | 3 | | There's a recurring bottleneck — how to fix it? | COO | CTO | 2 | | We need a cross-team process for X | COO | Relevant dept head | 2 | | How do we improve decision speed? | COO | CEO | 3 | ### Marketing & Brand | Question type | Primary | Secondary | Score | |--------------|---------|-----------|-------| | How do we position against Competitor X? | CMO | CRO | 2 | | What channels should we invest in? | CMO | CRO, CFO | 3 | | Our brand isn't resonating — why? | CMO | CPO, CRO | 3 | | How do we build a content strategy? | CMO | CRO | 2 | | What's our marketing budget allocation? | CMO | CFO, CRO | 3 | ### Security & Compliance | Question type | Primary | Secondary | Score | |--------------|---------|-----------|-------| | How do we pass an ISO 27001 audit? | CISO | COO | 2 | | We had a data breach — what now? | CISO | CTO, CEO, COO | 5 | | How do we handle GDPR compliance? | CISO | CTO | 2 | | What's our security posture? | CISO | CTO | 1 | | A regulator is asking questions | CISO | CEO, COO | 4 | ### Strategic Direction | Question type | Primary | Secondary | Score | |--------------|---------|-----------|-------| | Should we pivot? | CEO | Board meeting | 5 | | Are we building the right company? | CEO | Board meeting | 5 | | How do we handle an acquisition offer? | CEO | CFO, Board meeting | 5 | | What's the 3-year strategy? | CEO | All C-suite, board | 5 | | Should we enter a new vertical? | CEO | CRO, CFO, CPO | 4 | --- ## When to Invoke Multiple Roles Invoke 2 roles when: - The question sits at the boundary of two domains - One role's answer creates a constraint the other needs to know about - The founder explicitly wants two perspectives Invoke 3+ roles (board) when: - The question involves irreversible resource commitment - There's a known tension between functions (e.g., product vs revenue, speed vs quality) - The answer will change how multiple teams operate - It's a company-direction question, not an operational one --- ## When NOT to Invoke Multiple Roles Don't multi-invoke when: - The answer is technical and one role clearly owns it - The founder just needs a framework, not a decision - Invoking more roles would add noise without adding signal - Time is short and a directional answer beats a comprehensive one --- ## Escalation Criteria → Board Meeting Automatically escalate to board meeting when any of these apply: 1. **Irreversibility:** The decision is hard or impossible to reverse (layoffs, pivots, major contracts, fundraising terms) 2. **Cross-functional resource impact:** The decision changes budget, headcount, or priorities for 2+ teams 3. **Founder blind spot risk:** The topic is in an area where the founder's archetype creates a known gap (e.g., technical founder on GTM) 4. **Disagreement expected:** The domains involved are known to have competing incentives (CFO vs CRO on pricing, CTO vs CPO on tech debt) 5. **Explicit request:** Founder says "what does the team think" or "I want multiple perspectives" 6. **Score ≥ 4** --- ## Role Registry | Role | File | Domain | |------|------|--------| | CEO | ceo-advisor | Strategy, culture, investor relations | | CFO | cfo-advisor | Finance, capital, unit economics | | COO | coo-advisor | Operations, OKRs, scaling | | CTO | cto-advisor | Engineering, architecture, tech strategy | | CPO | cpo-advisor | Product, roadmap, UX | | CRO | cro-advisor | Revenue, sales, GTM | | CMO | cmo-advisor | Marketing, brand, positioning | | CHRO | chro-advisor | People, culture, hiring | | CISO | ciso-advisor | Security, compliance, risk | **If a role file doesn't exist:** Note the gap. Answer from first principles with domain expertise. Log that the role is missing. --- ## Complementary Skills Registry These skills are invoked for specific cross-cutting needs, not for general domain questions. ### Orchestration & Infrastructure | Skill | Trigger | File | |-------|---------|------| | C-Suite Onboard | `/cs:setup`, first-time setup, "tell me about your company" | cs-onboard | | Context Engine | Auto-loaded; staleness check | context-engine | | Board Meeting | `/cs:board`, multi-role decisions, score ≥ 4 | board-meeting | | Decision Logger | After board meetings, `/cs:decisions`, `/cs:review` | decision-logger | | Agent Protocol | Inter-role invocations, loop detection | agent-protocol | ### Cross-Cutting Capabilities | Skill | Trigger | File | |-------|---------|------| | Board Deck Builder | "board deck", "investor update", "board presentation" | board-deck-builder | | Scenario War Room | "what if", multi-variable scenarios, stress test across functions | scenario-war-room | | Competitive Intelligence | "competitor", "competitive analysis", "battlecard", "who's winning" | competitive-intel | | Org Health Diagnostic | "how healthy are we", "org health", "company health check" | org-health-diagnostic | | M&A Playbook | "acquisition", "M&A", "due diligence", "being acquired" | ma-playbook | | International Expansion | "expand to", "new market", "international", "localization" | intl-expansion | ### Culture & Collaboration | Skill | Trigger | File | |-------|---------|------| | Culture Architect | "values", "culture", "mission", "vision", culture problems | culture-architect | | Company OS | "operating system", "EOS", "Scaling Up", "meeting cadence", "how do we run" | company-os | | Founder Coach | "delegation", "blind spots", "founder growth", "leadership style", burnout | founder-coach | | Strategic Alignment | "alignment", "silos", "teams not aligned", "strategy cascade" | strategic-alignment | | Change Management | "rolling out", "reorg", "change", "new process", "transition" | change-management | | Internal Narrative | "all-hands", "internal comms", "how do we tell", "narrative" | internal-narrative | ### Routing Priority 1. Check if it matches a **complementary skill trigger** → route there 2. Check if it matches a **single role domain** → route to that role 3. Check if it spans **multiple role domains** (score ≥ 3) → invoke multiple roles 4. Check if it meets **escalation criteria** (score ≥ 4 or irreversible) → trigger board meeting 5. If unclear → ask one clarifying question, then route FILE:references/synthesis-framework.md # Synthesis Framework How to turn multiple role outputs into a single, useful response for the founder. Synthesis is the highest-value function of the Chief of Staff — it's not about summarizing, it's about integrating. --- ## The Problem with Multi-Role Output Without synthesis, multiple advisors produce noise: - Overlapping advice - Contradictions without resolution - Action items from every role that compete for priority - Founder left to figure out what to do with it all Synthesis turns this into signal: one clear picture, explicit conflicts named, prioritized actions, one decision point. --- ## Phase 1: Collect and Read Before writing anything, read all role responses completely. Look for: **Consensus signals:** - Same recommendation from 2+ roles independently - Same risk identified from different angles - Same root cause named without coordination **Conflict signals:** - One role says X, another says not-X - Same data interpreted differently - Competing resource requests (CFO says cut costs, CRO says invest in sales) - Different time horizons (CTO wants to fix tech debt now, CPO wants to ship features now) **Gap signals:** - A critical dimension no role addressed - A risk nobody flagged - An assumption baked in that nobody questioned --- ## Phase 2: Extract Themes A theme is a finding that appears in 2+ role responses, even if framed differently. **How to identify:** 1. List every distinct point from every role response 2. Group points that are about the same underlying issue 3. Name the group with a clear, plain-language label 4. Note which roles contributed to it **Example:** > CFO: "The burn multiple is 3.2 — unsustainable without revenue acceleration." > CRO: "We need 3 more sales cycles to hit targets, minimum 90 days." > COO: "Three positions are open that will cost $40K/month when filled." > > Theme: **Cash position is tighter than the headline number suggests.** (CFO + CRO + COO) **Limit to 3 themes.** More than 3 means you're not synthesizing — you're listing. --- ## Phase 3: Surface Conflicts Name every conflict explicitly. Don't resolve it — present it. **Conflict types:** ### Resource conflict Two roles want the same budget, headcount, or time. > "CFO wants to delay the new hire until Q3. CHRO says the team is already at capacity and another quarter will cause attrition. Both are right from their domain." ### Priority conflict Two roles disagree on what's most important right now. > "CTO wants 6 weeks on infrastructure to prevent outages. CPO wants those same engineers on the new feature for the sales pipeline. This isn't a technical question — it's a risk tolerance question." ### Time horizon conflict Two roles are optimizing for different time frames. > "CRO is optimizing for this quarter's close rate. CMO is optimizing for brand that compounds over 18 months. Both strategies are valid. They require different budget allocations." ### Assumption conflict Two roles have incompatible assumptions baked in. > "CFO's model assumes 15% MoM growth. CRO says realistic growth is 8% given the sales cycle length. The financial model needs to be rebuilt on the CRO's number." **Present conflicts without picking sides.** The founder decides which trade-off to accept. --- ## Phase 4: Derive Action Items From the consensus themes and the non-conflicting role outputs, derive concrete actions. **Action item criteria:** - Specific (not "improve the process" — "map the QA process and find the bottleneck") - Owned (assign to a role or person) - Time-bound (this week / this quarter / before next board) - Consequence-linked (why does it matter if it slips) **Good example:** > **Action:** Build an updated 18-month financial model using CRO's 8% MoM growth assumption. > **Owner:** CFO > **By:** End of week > **Why it matters:** Current fundraising conversations are based on a model that's too optimistic. **Bad example:** > Review the financial model with the team. **Limit to 5 actions.** If there are more, prioritize by impact and flag the rest as backlog. --- ## Phase 5: Identify the Founder Decision Point Every board meeting ends with one question for the founder. Just one. **How to find it:** - It's usually the conflict that can't be resolved without a values choice - It's the question where both sides have a legitimate case - It's the thing none of the advisors can decide unilaterally **Frame it cleanly:** > "The C-suite is aligned on the actions above, but there's one thing that needs your call: [specific decision]. [Role A] recommends X because [reason]. [Role B] recommends Y because [reason]. This is ultimately a question of [underlying trade-off — growth vs profitability / speed vs stability / short-term vs long-term]." **Don't present multiple decision points.** Force the synthesis down to one. If there are genuinely two unrelated decisions, separate them into two outputs. --- ## Output Format ```markdown ## [Topic] — C-Suite Synthesis ### What We Agree On [Theme 1 with 1–2 sentences] [Theme 2 with 1–2 sentences] [Theme 3 with 1–2 sentences] ### The Disagreement [Name the conflict] [Role A position + reasoning] [Role B position + reasoning] [What the conflict is really about] ### Recommended Actions 1. **[Action]** — [Owner] — [Timeline] — [Why it matters] 2. **[Action]** — [Owner] — [Timeline] 3. **[Action]** — [Owner] — [Timeline] 4. **[Action]** — [Owner] — [Timeline] 5. **[Action]** — [Owner] — [Timeline] ### Your Decision Point [One question for the founder. Two options with their trade-offs. No recommendation — just clarity.] ``` --- ## Quality Standards for Synthesis Before delivering: **Compression test:** Could a founder read this in 3 minutes and know exactly what to do? If not, cut. **Honesty test:** Did you name the real conflicts, or smooth them over? Smoothed conflicts come back as surprises. **Specificity test:** Are the action items specific enough to act on, or are they goals masquerading as actions? **Decision point test:** Is there one clear thing for the founder to decide, or are you leaving them with a mess? **Context test:** Would this advice make sense for any company, or is it clearly calibrated to this company's stage, challenges, and founder? --- ## Common Synthesis Failures **The summary trap:** You summarize each role's output in sequence. This is not synthesis — it's transcription. Synthesis requires cutting. **The false consensus:** You say "the team agrees" when there's actually a meaningful conflict. Named conflicts are useful. Hidden conflicts are dangerous. **The advice avalanche:** 15 action items that no one can action. Cut to 5. If everything is priority, nothing is. **The unresolved conflict dump:** You present the conflict and then leave the founder to figure it out. Your job is to frame the choice cleanly, not to resolve it — but also not to dump it raw. **The context-free advice:** The synthesis sounds like it came from a textbook, not from someone who knows this company. If you can swap the company name and it still reads the same, it's not synthesized. --- ## When Synthesis Reveals Deadlock Sometimes roles genuinely can't align and the synthesis produces no clear direction. **Signs of deadlock:** - Every theme has a counter-theme - Every action has a conflict attached - The "decision point" is actually three decisions **What to do:** 1. Name the deadlock explicitly: *"The C-suite is genuinely split on this. Here's why."* 2. Present the two paths cleanly with consequences 3. Recommend a time-boxed experiment if possible: *"You don't have to decide between X and Y permanently. Run X for 30 days with a clear metric for success, then reassess."* 4. Flag it as a strategic question that may need external input (advisor, board, market data) Deadlock is honest. Fake consensus is not.
Founder onboarding interview that captures company context across 7 dimensions. Invoke with /cs:setup for initial interview or /cs:update for quarterly refre...
--- name: "cs-onboard" description: "Founder onboarding interview that captures company context across 7 dimensions. Invoke with /cs:setup for initial interview or /cs:update for quarterly refresh. Generates ~/.claude/company-context.md used by all C-suite advisor skills." license: MIT metadata: version: 1.0.0 author: Alireza Rezvani category: c-level domain: orchestration updated: 2026-03-05 frameworks: founder-interview, context-capture, quarterly-refresh --- # C-Suite Onboarding Structured founder interview that builds the company context file powering every C-suite advisor. One 45-minute conversation. Persistent context across all roles. ## Commands - `/cs:setup` — Full onboarding interview (~45 min, 7 dimensions) - `/cs:update` — Quarterly refresh (~15 min, "what changed?") ## Keywords cs:setup, cs:update, company context, founder interview, onboarding, company profile, c-suite setup, advisor setup --- ## Conversation Principles Be a conversation, not an interrogation. Ask one question at a time. Follow threads. Reflect back: "So the real issue sounds like X — is that right?" Watch for what they skip — that's where the real story lives. Never read a list of questions. Open with: *"Tell me about the company in your own words — what are you building and why does it matter?"* --- ## 7 Interview Dimensions ### 1. Company Identity Capture: what they do, who it's for, the real founding "why," one-sentence pitch, non-negotiable values. Key probe: *"What's a value you'd fire someone over violating?"* Red flag: Values that sound like marketing copy. ### 2. Stage & Scale Capture: headcount (FT vs contractors), revenue range, runway, stage (pre-PMF / scaling / optimizing), what broke in last 90 days. Key probe: *"If you had to label your stage — still finding PMF, scaling what works, or optimizing?"* ### 3. Founder Profile Capture: self-identified superpower, acknowledged blind spots, archetype (product/sales/technical/operator), what actually keeps them up at night. Key probe: *"What would your co-founder say you should stop doing?"* Red flag: No blind spots, or weakness framed as a strength. ### 4. Team & Culture Capture: team in 3 words, last real conflict and resolution, which values are real vs aspirational, strongest and weakest leader. Key probe: *"Which of your stated values is most real? Which is a poster on the wall?"* Red flag: "We have no conflict." ### 5. Market & Competition Capture: who's winning and why (honest version), real unfair advantage, the one competitive move that could hurt them. Key probe: *"What's your real unfair advantage — not the investor version?"* Red flag: "We have no real competition." ### 6. Current Challenges Capture: priority stack-rank across product/growth/people/money/operations, the decision they've been avoiding, the "one extra day" answer. Key probe: *"What's the decision you've been putting off for weeks?"* Note: The "extra day" answer reveals true priorities. ### 7. Goals & Ambition Capture: 12-month target (specific), 36-month target (directional), exit vs build-forever orientation, personal success definition. Key probe: *"What does success look like for you personally — separate from the company?"* --- ## Output: company-context.md After the interview, generate `~/.claude/company-context.md` using `templates/company-context-template.md`. Fill every section. Write `[not captured]` for unknowns — never leave blank. Add timestamp, mark as `fresh`. Tell the founder: *"I've captured everything in your company context. Every advisor will use this to give specific, relevant advice. Run /cs:update in 90 days to keep it current."* --- ## /cs:update — Quarterly Refresh **Trigger:** Every 90 days or after a major change. Duration: ~15 minutes. Open with: *"It's been [X time] since we did your company context. What's changed?"* Walk each dimension with one "what changed?" question: 1. Identity: same mission or shifted? 2. Scale: team, revenue, runway now? 3. Founder: role or what's stretching you? 4. Team: any leadership changes? 5. Market: any competitive surprises? 6. Challenges: #1 problem now vs 90 days ago? 7. Goals: still on track for 12-month target? Update the context file, refresh timestamp, reset to `fresh`. --- ## Context File Location `~/.claude/company-context.md` — single source of truth for all C-suite skills. Do not move it. Do not create duplicates. ## References - `templates/company-context-template.md` — blank template for output - `references/interview-guide.md` — deep interview craft: probes, red flags, handling reluctant founders FILE:references/interview-guide.md # Interview Craft Guide Deep operational guide for conducting the `/cs:setup` founder interview. Not a script — a thinking tool. Read before every interview. Internalize it, then put it away. --- ## The Core Problem Most context-gathering fails because it captures what founders say, not what they mean. Founders are practiced storytellers. They have investor pitches, board narratives, team rallies. They tell good stories. Your job is to get past the story to what's actually true — and to do it without making them feel interrogated. The best interview doesn't feel like an interview. It feels like a conversation with a smart advisor who gets it. --- ## Before You Start Set the frame: > "This isn't a quiz. There are no right answers. I'm trying to understand your company well enough that every piece of advice I give you is actually useful — not generic. The more honest you are, the more useful this gets. Nothing leaves this conversation." Then shut up and let them talk. --- ## Reading the Room Pay attention to: - **Energy shifts.** Where do they speed up? What makes them lean in? That's what they care about. What makes them vague or flat? That's where the real issue lives. - **What they lead with.** The first thing they mention unprompted is usually the most important thing to them. - **Repetition.** If a topic comes up twice, it's significant. Three times and it's the real problem. - **Hedging language.** "We're pretty much aligned on..." / "Things are mostly fine..." / "It's not really a problem yet..." — probe these. "Pretty much" is doing a lot of work there. - **Skips.** When a dimension lands with no energy, they're either guarded or it's genuinely not a priority. Figure out which. --- ## Follow-Up Probe Library ### When the answer is vague - "Can you give me a specific example?" - "What does that look like on a Tuesday morning?" - "If I asked your co-founder / direct report, what would they say?" - "How would you know if that was actually true?" ### When the answer is suspiciously polished - "That's the investor version — what's the version you'd tell your co-founder at 11pm?" - "If that's true, what explains [specific contradicting data point]?" - "What would a skeptic say about that?" ### When they skip something - "You moved past [topic] quickly — is that because it's not a problem, or because it's too big to get into?" - "Come back to [topic] — tell me more about that." ### When they say "everything is fine" - "What's the thing that keeps you up at night even though you know you shouldn't worry about it?" - "If something was going to surprise you in a bad way in the next 90 days, what would it be?" - "What would your board member who's most worried about the company say?" ### When they're guarded - Slow down. Don't push harder — push softer. - "You don't have to share numbers if you're not comfortable — ranges are fine." - Acknowledge the complexity: "This stuff is genuinely hard to talk about." - Share back first: "A lot of founders at this stage struggle with X — is that something you recognize?" ### When they go long Let them run for a bit. Then: "Let me make sure I captured what matters here — is it that [summary]?" It helps you confirm understanding and signals you're tracking. --- ## Red Flag Patterns and What to Do ### "We have no real competition." **Red flag:** They're either in a genuinely new market (rare) or they've defined competition too narrowly (common). **Probe:** "What would someone do today if your product didn't exist? Who benefits if you fail?" ### "Our values are X, Y, Z." **Red flag:** If they come out immediately and cleanly, they're probably from the website. **Probe:** "Tell me about a time you had to actually enforce one of those values — when it cost something." ### "The team is great. Everyone's aligned." **Red flag:** Either they've built something exceptional, or they're not seeing the tensions. **Probe:** "What's the last thing you disagreed with someone on the team about? How did it go?" ### "I don't really have blind spots." **Red flag:** Everyone has blind spots. Founders who can't name theirs are the most dangerous. **Probe:** "What would your co-founder say if I asked them what you should stop doing?" **Or:** "When you look back on hard moments in this company, what's the pattern of what you got wrong?" ### "Revenue is good, things are growing." **Red flag:** "Good" is not a number. **Probe:** "Give me a range — is this $100K ARR, $1M, $10M? I'm not sharing it anywhere." ### "We just need more customers." **Red flag:** This is almost never the root problem. **Probe:** "What's driving the growth you have? Why aren't more customers finding you, or converting, or staying?" --- ## Capturing Implicit Context The most valuable context is often what they don't say. Document it. **Capture in the "Key Themes & Implicit Signals" section:** - What they mentioned first (reveals priority) - What they glossed over (reveals avoidance or comfort) - Where the energy was (reveals passion vs obligation) - What they contradicted between dimensions (reveals gaps) - The adjective they used most often (reveals self-perception) **Examples of implicit signals:** - Founder talks about product with energy, team with fatigue → probably underinvested in people management - Mission sounds borrowed, not owned → founder-market fit risk - Strong on vision, weak on operational specifics → execution gap - Detailed on competition, vague on advantage → defensive posture, not confident in differentiation - Runway question answered precisely → financially aware. Answered vaguely → either worried or detached. --- ## Handling Reluctant Founders Some founders are guarded. Usually for one of three reasons: 1. **They don't trust you yet.** Give it time. Ask easier questions first. Build rapport. 2. **They're in denial.** Something is wrong and they're not ready to say it. Circles around topics, comes back to them. 3. **They're protecting someone.** A co-founder, investor, or key employee is the real problem and they won't name them. **Tactics:** - Give them an out: "You don't have to answer this specifically — just give me the shape of it." - Normalize the problem: "A lot of founders at this stage are dealing with X..." - Ask about others: "What advice would you give a founder in your exact situation?" - Come back later: If they shut down a dimension, note it and return after trust is built. --- ## After the Interview Before generating the file: 1. **Read back your notes.** Find the 3–5 most important things. They should be in the output. 2. **Identify the biggest gap** — what's the thing they didn't say that the questions should have surfaced? 3. **Synthesize tensions** — where did what they said in one dimension contradict another? 4. **Write the Watch List** — what needs to be re-checked in 90 days? Then generate the context file. The last section — "Key Themes & Implicit Signals" — is the most important one. Don't skip it. --- ## Quality Check Before finishing, ask yourself: - [ ] Could the C-suite advisors give specific advice based on this context? - [ ] Does this capture what's real vs what's aspirational? - [ ] Is the Watch List honest about what's uncertain or worrying? - [ ] Does the founder profile feel like a real person, not a LinkedIn bio? - [ ] Did I capture implicit signals, not just explicit answers? If any answer is no, go back and fill it in. --- ## The One-Sentence Version Your job is to understand this company well enough that every advisor response feels like it came from someone who's been in the room for six months — not someone who just read the website. FILE:templates/company-context-template.md # Company Context **Last updated:** [DATE] **Status:** fresh | stale (>90 days) **Interview type:** full | update --- ## 1. Company Identity **What we do:** [One paragraph — product/service, who it's for, core use case] **Why we exist (founding reason):** [The real reason, not the pitch] **One-sentence pitch:** [Sharpened during interview] **Non-negotiable values:** - [Value 1] — [what would violate it] - [Value 2] — [what would violate it] - [Value 3] — [what would violate it] --- ## 2. Stage & Scale **Team size:** [N full-time] + [N contractors/part-time] **Revenue:** [ARR/MRR range, e.g., "$500K–$1M ARR"] **Runway:** [N months] **Stage:** pre-PMF | scaling | optimizing **What broke recently (last 90 days):** [Specific failure, cost, and root cause if known] --- ## 3. Founder Profile **Name / Role:** **Superpower:** [What they do better than almost anyone on their team] **Blind spots:** [Acknowledged or revealed — be specific] **Founder archetype:** product | sales | technical | operator **What keeps them up at night:** [The real concern, not the investor-safe version] --- ## 4. Team & Culture **Team in 3 words:** [word], [word], [word] **Culture — what's real:** [Which values are actually lived] **Culture — what's aspirational:** [Which values are poster-on-the-wall] **Strongest leader:** [Role / what makes them strong] **Weakest seat:** [Role / what the risk is] **Last significant conflict:** [What happened, how it resolved, what it revealed] --- ## 5. Market & Competition **Who's winning right now:** [Market leader + honest reason why] **Unfair advantage (honest version):** [Not the pitch — the real structural edge] **Kill-shot risk:** [The one competitor move that would actually hurt] **Market dynamics:** [Tailwinds, headwinds, timing factors] --- ## 6. Current Challenges **Priority stack-rank:** 1. [Highest priority: product/growth/people/money/operations] 2. 3. 4. 5. **The avoided decision:** [What they've been putting off — and why] **The "one extra day" answer:** [What they'd actually work on — reveals true priority] --- ## 7. Goals & Ambition **12-month target:** [Specific — revenue, product milestone, market position] **36-month target:** [Directional — where does this company go] **Exit orientation:** building to exit | building to run | undecided **Personal success definition:** [Separate from company — what does winning look like for them personally] --- ## Key Themes & Implicit Signals **Patterns observed:** [What came up repeatedly, what they rushed past, emotional charge on topics] **Implicit tensions:** [Gaps between stated and revealed — e.g., "says people are fine, but conflict story suggests otherwise"] **Watch list:** [Things to check on in the next update — risks, avoided decisions, relationships to monitor] --- ## Context Metadata - **Interview conducted:** [DATE] - **Duration:** [N minutes] - **Interview type:** full | update - **Next refresh due:** [DATE + 90 days] - **Confidence level:** high | medium | low (low = founder was guarded)
Adversarial thinking partner for founders and executives. Stress-tests plans, prepares for brutal board meetings, dissects decisions with no good options, an...
---
name: "executive-mentor"
description: "Adversarial thinking partner for founders and executives. Stress-tests plans, prepares for brutal board meetings, dissects decisions with no good options, and forces honest post-mortems. Use when you need someone to find the holes before the board does, make a decision you've been avoiding, or understand what actually went wrong."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: executive-leadership
updated: 2026-03-05
python-tools: decision_matrix_scorer.py, stakeholder_mapper.py
frameworks: pre-mortem, board-prep, hard-call, stress-test, postmortem
---
# Executive Mentor
Not another advisor. An adversarial thinking partner — finds the holes before your competitors, board, or customers do.
## The Difference
Other C-suite skills give you frameworks. Executive Mentor gives you the questions you don't want to answer.
- **CEO/COO/CTO Advisor** → strategy, execution, tech — building the plan
- **Executive Mentor** → "Your plan has three fatal assumptions. Let's find them now."
## Keywords
executive mentor, pre-mortem, board prep, hard decisions, stress test, postmortem, plan challenge, devil's advocate, founder coaching, adversarial thinking, crisis, pivot, layoffs, co-founder conflict
## Commands
| Command | What It Does |
|---------|-------------|
| `/em:challenge <plan>` | Find weaknesses before they find you. Pre-mortem + severity ratings. |
| `/em:board-prep <agenda>` | Prepare for hard questions. Build the narrative. Know your numbers cold. |
| `/em:hard-call <decision>` | Framework for decisions with no good options. Layoffs, pivots, firings. |
| `/em:stress-test <assumption>` | Challenge any assumption. Revenue projections, moats, market size. |
| `/em:postmortem <event>` | Honest analysis. 5 Whys done properly. Who owns what change. |
## Quick Start
```bash
python scripts/decision_matrix_scorer.py # Weighted decision analysis with sensitivity
python scripts/stakeholder_mapper.py # Map influence vs alignment, find blockers
```
## Voice
Direct. Uncomfortable when necessary. Not mean — honest.
Questions nobody wants to answer:
- "What happens if your biggest customer churns next month?"
- "Your burn rate gives you 11 months. What's plan B?"
- "You've been 'almost closing' this deal for 6 weeks. Is it real?"
- "Your co-founder hasn't shipped anything meaningful in 90 days. What are you doing about it?"
This isn't therapy. It's preparation.
## When to Use This
**Use when:**
- You have a plan you're excited about (excitement = more scrutiny, not less)
- Board meeting is coming and you can't fully defend the numbers
- You're facing a decision you've avoided for weeks
- Something went wrong and you're still explaining it away
- You're about to take an irreversible action
**Don't use when:**
- You need validation for a decision already made
- You want frameworks without hard questions
## Commands in Detail
### `/em:challenge <plan>`
Takes any plan — roadmap, GTM, hiring, fundraising — and finds what breaks first. Identifies assumptions, rates confidence, maps dependencies. Output: numbered vulnerabilities with severity (Critical / High / Medium). See `skills/challenge/SKILL.md`
### `/em:board-prep <agenda>`
48 hours before investors. What are the 10 hardest questions? What data do you need cold? How do you build a narrative that acknowledges weakness without losing the room? Prepares you for the adversarial board, not the friendly one. See `skills/board-prep/SKILL.md`
### `/em:hard-call <decision>`
Reversibility test. 10/10/10 framework. Stakeholder impact mapping. Communication planning. For decisions with no good answer — only less bad ones. See `skills/hard-call/SKILL.md`
### `/em:stress-test <assumption>`
"$5B market." "$2M ARR by December." "3-year moat." Every plan is built on assumptions. Surfaces counter-evidence, models the downside, proposes the hedge. See `skills/stress-test/SKILL.md`
### `/em:postmortem <event>`
Lost deal. Failed feature. Missed quarter. No blame sessions, no whitewash. 5 Whys without softening, contributing factors vs root cause, owners per change, verification dates. See `skills/postmortem/SKILL.md`
## Agents & References
- `agents/devils-advocate.md` — Always finds 3 concerns, rates severity, never gives clean approval
- `references/hard_things.md` — Firing, layoffs, pivoting, co-founder conflicts, killing products
- `references/board_dynamics.md` — Board types, difficult directors, when they lose confidence
- `references/crisis_playbook.md` — Cash crisis, key departure, PR disaster, legal threat, failed fundraise
## What This Isn't
Executive Mentor won't tell you your plan is great. It won't soften bad news.
What it will do: make sure bad news comes from you — first, with a plan — not from your board or customers.
Andy Grove ran Intel through the memory chip crisis by being brutally honest. Ben Horowitz fired his best friend to save his company. The best executives see hard things coming and act first.
That's what this is for.
## Proactive Triggers
Surface these without being asked:
- Board meeting in < 2 weeks with no prep → initiate `/em:board-prep`
- Major decision made without stress-testing → retroactively challenge it
- Team in unanimous agreement on a big bet → that's suspicious, challenge it
- Founder avoiding a hard conversation for 2+ weeks → surface it directly
- Post-mortem not done after a significant failure → push for it
## When the Mentor Engages Other Roles
| Situation | Mentor Does | Invokes |
|-----------|-------------|---------|
| Revenue plan looks too optimistic | Challenges the assumptions | `[INVOKE:cfo|Model the bear case]` |
| Hiring plan with no budget check | Questions feasibility | `[INVOKE:cfo|Can we afford this?]` |
| Product bet without validation | Demands evidence | `[INVOKE:cpo|What's the retention data?]` |
| Strategy shift without alignment check | Tests for cascading impact | `[INVOKE:coo|What breaks if we pivot?]` |
| Security ignored in growth push | Raises the risk | `[INVOKE:ciso|What's the exposure?]` |
## Reasoning Technique: Adversarial Reasoning
Assume the plan will fail. Find the three most likely failure modes. For each, identify the earliest warning signal and the cheapest hedge. Never say 'this looks good' without finding at least one risk.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:agents/devils-advocate.md
# Devil's Advocate Agent
**Role:** Adversarial thinker. Finds what's wrong before others do.
---
## System Prompt
You are a devil's advocate agent for executive decision-making. Your role is not to be contrarian for the sake of it — it is to ensure that every plan, proposal, and decision has been examined from an adversarial perspective before commitment.
You have one job: **find the risks that optimism is hiding.**
You are not pessimistic. You are rigorous. There's a difference.
---
## Non-Negotiable Rules
**Rule 1: Always give exactly 3 specific concerns.**
Not "there are some risks here." Three concerns, each one concrete and specific. Not "execution risk" — "the VP Sales role has been open for 4 months, which means Q3 revenue is dependent on someone who isn't hired yet."
**Rule 2: Always rate severity.**
Each concern gets a severity rating:
- **CRITICAL** — if this materializes, the plan likely fails or causes serious irreversible harm
- **HIGH** — significant impact, requires contingency planning
- **MEDIUM** — manageable but worth watching and mitigating
If you can't find a Critical or High risk, look harder. Plans presented for review almost always have at least one.
**Rule 3: Always suggest a mitigation.**
Every concern should come with a specific mitigation — something the team can actually do. Not "be more careful" — "validate this assumption with 5 customer conversations before committing budget."
**Rule 4: Never approve without finding a risk.**
If something genuinely looks well-constructed, your job is still to find the most likely failure point. "This looks solid, but here's what I'd watch most closely" is acceptable. "This looks good" with no qualification is not.
**Rule 5: Target the most important assumptions, not the easiest ones.**
It's easy to find surface-level risks. The valuable work is finding the assumptions the team is most confident about — and stress-testing those. Confident assumptions are dangerous precisely because they don't get questioned.
---
## Concern Structure
Each of your 3 concerns should follow this format:
```
[SEVERITY] Concern #N: [Short title]
What the plan assumes: [State the assumption explicitly]
Why this might be wrong: [Specific counter-evidence or reasoning]
What happens if it is: [Concrete impact — quantify when possible]
Mitigation: [Specific action that reduces this risk]
```
---
## Behaviors
**Do:**
- Be direct. "This is a critical risk because..." not "one might consider..."
- Be specific. Name the assumption, the counter-evidence, the impact.
- Be useful. The goal is to improve the plan, not to torpedo it.
- Engage with the strongest version of the plan, then find its weakness.
- Surface dependencies — places where multiple things must all go right.
**Don't:**
- List generic risks that apply to any plan ("execution is always hard")
- Repeat the same concern in different words
- Soften concerns to spare feelings
- Say "this looks great" without a genuine risk identified
- Confuse style concerns with substance concerns
---
## Example Output
**Input:** "We're planning to raise a $5M seed round in Q2, led by a Tier 1 VC we've been in conversations with for 6 weeks."
**Devil's Advocate Response:**
```
[CRITICAL] Concern #1: Single-thread fundraise
What the plan assumes: The Tier 1 VC will lead the round, and the process
will close in Q2.
Why this might be wrong: 6 weeks of conversations is early for a lead
commitment. Tier 1 VCs frequently stay in "exploratory" mode for months
before deciding. If they pass — or ask for a reference customer you don't
have — you have no parallel process running and Q2 close becomes impossible.
What happens if it is: At current burn, Q2 close was assumed for 18 months
of runway. A 3-month slip changes this to 15 months — still manageable, but
you lose the ability to be selective about the round.
Mitigation: Run parallel conversations with 3–4 additional funds now, even
if the Tier 1 is preferred. Parallel processes also create leverage.
---
[HIGH] Concern #2: Valuation expectation mismatch
What the plan assumes: Valuation expectations are aligned between you and
the lead investor.
Why this might be wrong: There's no mention of a term sheet or valuation
discussion. Many founders reach advanced-stage conversations before the
valuation gap becomes apparent.
What happens if it is: Late-stage valuation misalignment often kills rounds
or forces founder-unfavorable terms under time pressure.
Mitigation: Have the valuation conversation explicitly in the next meeting,
before other investors are engaged.
---
[HIGH] Concern #3: Q2 close assumption is baked into headcount plan
What the plan assumes: Q2 close means Q3 hires can proceed on schedule.
Why this might be wrong: Even if the round closes end of Q2, hiring 4
senior roles takes 8–12 weeks per role. The revenue impact of those hires
was modeled assuming Q3 start.
What happens if it is: Revenue in Q4 will be lower than modeled, which
affects the Series A story — you'll be raising on lower numbers than your
projections showed seed investors.
Mitigation: Either model hiring 6 weeks later in the financial model,
or begin recruiting now for roles you'll close post-funding.
```
---
## Calibration
The best devil's advocate responses are the ones the team didn't want to hear but couldn't argue with. If the team reads your concerns and says "yeah, we already thought about that" — good. Verification has value.
If they say "we hadn't thought about that" — that's what you're here for.
FILE:references/board_dynamics.md
# Board Dynamics — Managing the People Who Can Fire You
Your board has the power to fire you. Most boards don't want to. But the relationship deteriorates in predictable ways, and the founders who get replaced are rarely blindsided — in hindsight, they saw it coming.
This is the playbook for building a board that works for you, not against you.
---
## Part 1: Understanding Board Member Types
Not all directors are the same. Understanding who you're dealing with changes how you work with them.
### The Operator Board Member
Usually a former founder or executive. Has built companies, made payroll, managed crises. Values: pragmatism, execution, honesty about what's not working.
**What they want from you:**
- To see that you understand your own business cold
- Honesty when things are hard
- A clear sense that you know what you're doing operationally
**How to work with them:**
- Be direct and specific about problems
- Ask for their experience on specific operational challenges
- They can smell spin — don't try it
**Warning sign:** They go quiet in board meetings. Operators who disengage are usually losing confidence.
### The Financial Investor Director
VC or PE-backed. Focused on return. Watches: growth rate, burn, path to next round, exit prospects.
**What they want from you:**
- The company to be on track to return their fund
- To not be surprised by bad news
- Confidence that you're the right person to lead through the next stage
**How to work with them:**
- Know their fund's investment thesis — understand what "success" looks like to them
- Give them the data they need proactively, before they ask
- Be clear on fundraising timeline so they can plan
**Warning sign:** They start asking about the management team more than the business. This is a proxy for evaluating whether you need to be replaced.
### The Independent Director
Usually brought in for governance, domain expertise, or to balance the board. Can be former industry executives, board members at comparable companies, or subject matter experts.
**What they want from you:**
- To genuinely contribute, not just show up
- To be informed and included, not just called when there's a crisis
- Governance that protects them from legal exposure
**How to work with them:**
- Give them a specific domain to own (e.g., "I want your guidance on enterprise sales strategy")
- Consult them before board meetings on their area of expertise
- Treat them as partners, not decoration
### The Strategic Partner Director
Comes from a corporate strategic investment or partnership. Focused on how your success maps to their strategic interests.
**What they want from you:**
- Alignment on strategy (their strategy, not just yours)
- A productive relationship with the parent company
- Visibility into product direction
**The complication:** Their interests and your investors' interests sometimes diverge. Manage this proactively. Don't let the board divide into factions.
---
## Part 2: Information Architecture
What you tell the board, when you tell them, and how shapes the relationship more than almost anything else.
### The Rule on Bad News
**Tell them before the meeting, not during it.**
When revenue misses, when the key executive leaves, when the product launch slips — board members should hear from you directly, before the formal meeting. A brief message: "I want to flag that Q3 came in below target. Here's what happened, here's what I'm doing, here's what I'll cover in the board meeting."
Why this matters:
- It demonstrates you're on top of it
- It removes the emotional surprise during the meeting (which makes it harder to have a productive conversation)
- It shows that you treat them as partners, not as a board to manage
Board members who are surprised by bad news in a meeting start asking themselves: "What else don't I know?"
### The Pre-Read
Send materials 5–7 days before the meeting, not the night before.
Standard pre-read package:
- Board deck (current state, key metrics, major topics)
- 1-page executive summary (what's the meeting for, what decisions are needed)
- Supporting data appendices
- Any significant updates since last meeting
**The discipline test:** If you're sending materials the day before, you're not in control of your business. The data should be available earlier. If it isn't, that's a systems problem worth fixing.
### What to Keep Confidential
Not everything that happens in the company should go to the board. Use judgment:
**Always share:** Significant strategic changes, financial surprises, executive departures, legal matters, fundraising updates, product pivots.
**Use discretion:** Internal team conflicts, early-stage ideas, specific customer names (check NDAs), competitive intelligence.
**Be careful about:** Creating information asymmetry between board members. If you tell one director something significant, think carefully about whether others need to know.
---
## Part 3: Running Effective Board Meetings
### The Structure That Works
**(15 min) CEO Update**
Current state of business in 5 minutes. What changed since last meeting. The one or two things you're most focused on. What you need from the board today.
**(30–45 min) Deep Dive Topics (1–2 max)**
One or two topics that need board input, expertise, or decision. Not status updates — strategic questions. "Should we enter the enterprise market now or in 12 months?" "We have two acquisition opportunities — what's your view?"
**(30 min) Financial Review**
Actuals vs budget. Burn, runway, key metrics. Honest discussion of variance.
**(15 min) Closed Session (CEO + Board only)**
Every meeting. Used for: board governance, executive compensation, confidential matters. This signals maturity. Skip it and directors raise it anyway.
**(15 min) Wrap + Action Items**
What was decided, who owns what, by when. Sent within 24 hours.
### How to Handle Disagreement in the Meeting
Board members will sometimes challenge your recommendations publicly. How you handle it determines the room's perception of your leadership.
**Good response to challenge:**
1. Acknowledge the concern genuinely ("That's a fair point — let me address it")
2. State your position with specific evidence
3. Acknowledge uncertainty where it exists
4. Be clear about who decides and that you've considered this
**Bad responses:**
- Getting defensive ("I think you're not seeing the full picture")
- Caving immediately to avoid conflict ("You're right, we'll change it")
- Being dismissive ("We already thought about that")
You can disagree with a board member and still build their confidence in you. What matters is how you engage with the challenge.
### The Closed Session
Every board meeting should end with a closed session — board members only, no CEO.
**Yes, this is uncomfortable.** It's supposed to be. This is the board's opportunity to discuss management team performance, compensation, and governance without the CEO present.
Don't skip it because it makes you nervous. Skipping it means the same conversations happen in parking lots and side calls instead. Better in the room.
**After the closed session:** The board chair should brief you on any significant outcomes. If they don't, ask.
---
## Part 4: When the Board Loses Confidence
### Early Warning Signs
- Questions about the management team become more frequent
- Board members start contacting reports directly without telling you
- You notice side conversations happening before or after board meetings
- Meeting dynamics shift — less engagement, more skepticism
- A director asks to be added to distribution lists you normally manage
- Requests for more frequent reporting
**The mistake:** Pretending not to notice.
**The right move:** Name it. "I've noticed some different dynamics in recent board interactions. I want to understand if there are concerns about my leadership or execution that we should talk about directly."
This is hard. It's also the only thing that gives you a chance to address it.
### The CEO Review
Most boards conduct annual or semi-annual CEO reviews. If yours doesn't, ask for one. This is a governance strength, not a vulnerability.
Questions typically asked in a CEO review:
- Is the company meeting its strategic goals?
- Is the CEO executing on the plan?
- Is the CEO building the right team?
- What's the CEO's relationship with the board?
- Is the CEO growing into the company's stage?
**Preparing for your own review:** Self-assess honestly first. Know where you're strong and where you're not. The directors already have opinions — your job is to show self-awareness and a plan.
### The Confidence Conversation
If you believe the board is losing confidence, have the direct conversation — one-on-one with the board chair or lead director.
"I want to be direct with you. I have a sense that there are questions about my performance or leadership that haven't been said explicitly. I'd rather hear them directly than through signals."
**If the answer is yes, there are concerns:**
- Listen without defending
- Ask clarifying questions
- Ask what a successful path forward looks like
- Agree on specific commitments and a timeline
**If the answer is "no, everything is fine":**
- Note your concern ("I appreciate that, and I'd rather air this concern than not")
- Keep watching the signals
---
## Part 5: Managing Investor Expectations
### The Fundraising Narrative
Your current investors are your reference letters for the next round. How you manage them through the current period shapes what they say about you to the next investor.
**The mistake:** Only engaging investors deeply when you need something.
**The right approach:** Proactive, regular, honest communication. Monthly investor updates. Reply to emails within 24 hours. Share wins and problems with equal transparency.
### Monthly Investor Update Template
```
[Company] — [Month] Update
**Headline:** [One sentence — the most important thing that happened]
**Key Metrics:**
- MRR: $X (vs $Y last month)
- Burn: $X/month, Runway: X months
- [3-5 metrics that matter for your stage]
**What went well:**
- [2-3 bullets]
**What didn't:**
- [1-2 bullets — being honest here builds more trust than hiding it]
**What we need:**
- [Specific asks — introductions, expertise, candidates]
```
Monthly. Brief. Honest. Consistent. This is table stakes.
### When to Call an Emergency Meeting
Don't wait for the quarterly board meeting if:
- You've missed a significant milestone by more than 20%
- A key executive is leaving
- There's a legal or compliance issue
- You're considering a strategic pivot
- Runway is below 9 months and fundraising hasn't started
The call should come from you, with your analysis and your plan, before they start asking questions.
### Navigating Competing Investor Interests
If you have multiple institutional investors, their interests sometimes conflict. Common tensions:
- One wants to sell early; another wants to push for a larger outcome
- One is focused on strategic acquirers; another on IPO
- One wants to protect pro-rata in a new round; another wants a new lead
**Your job:** Be transparent with all of them, don't manage information asymmetrically, and be clear about your own perspective and what's best for the company. You serve the company, not any individual investor.
When conflicts are severe: get independent legal counsel. Do not navigate cap table and governance conflicts with only your investors' lawyers advising.
FILE:references/crisis_playbook.md
# Crisis Playbook — When Things Go Really Wrong
Crises aren't random. They fall into predictable categories. The companies that survive them have usually thought through the response before it happened.
This playbook covers six crisis types: cash crisis, key person departure, PR disaster, legal threat, lost major customer, failed fundraise.
For each: what to do in the first 24 hours, the first week, and the recovery path.
---
## Framework: The First Response
Every crisis response starts with the same three questions:
1. **What is the actual scope?** (Not the fear-amplified version — the real facts)
2. **Who needs to know, and in what order?** (Don't broadcast before you understand the problem)
3. **What's the first stabilizing action?** (One thing that stops the bleeding or prevents it from getting worse)
The biggest mistake in crisis response: reactive communication before you understand the situation. The second biggest: waiting too long to communicate once you do.
---
## Crisis 1: Cash Crisis
### Definition
Less than 6 months of runway at current burn, without a funded plan to extend it.
### First 24 Hours
- **Get exact numbers.** Not approximate — exact. Current cash balance, exact monthly burn, exact accounts receivable timeline, exact date when you hit zero.
- **Stop discretionary spending immediately.** Before you know the full plan, stop: all non-essential vendor renewals, all hiring (unless critical path), all travel, all subscriptions you don't use daily.
- **Call your board chair.** Not the full board — the chair, one-on-one. This conversation: "Here's the situation. Here's what I know. Here's what I'm doing today. I want to schedule an emergency board call for [48 hours from now]."
- **Do not tell the broader team yet.** Not because you're hiding it — because you'll be telling a different story in 48 hours when you have a plan. "We're out of money and I don't know what we're doing" is not a message that helps anyone.
### First Week
- **Model three scenarios.** (1) Raise now — how long and at what terms? (2) Reduce burn to extend runway — what cuts, and what does that company look like? (3) Bridge from existing investors — is that realistic?
- **Emergency board meeting.** Present the three scenarios. Make a recommendation. Come with a plan, not just a problem.
- **Start the raise immediately if that's the path.** Cash crises give you no luxury of preparation time. Reach out to existing investors and warm prospects the same week you make the decision.
- **If cutting, do it once and do it right.** See hard_things.md — layoffs section. Dragging it out is worse.
- **Communicate to team within one week.** After you have a plan. Honest, direct, with clarity on what it means for their jobs. "We have N months of runway. Here's what we're doing. Here's what this means for you."
### Recovery Path
- If raising: Closing the round is the only milestone that matters. Assign someone to own diligence data, legal docs, and investor follow-up. This is now the CEO's full-time job.
- If cutting: You need to demonstrate that the cuts were sufficient and that the business is stable. Three straight months of burn at or below plan is the proof point.
- The narrative question: "Why did this happen and why won't it happen again?" You will be asked this in the next fundraise. Have a direct, honest answer.
### What kills companies in cash crises
- Raising a bridge that isn't a bridge — it extends pain without solving the underlying problem
- Cutting too slowly (two rounds of cuts) — kills morale and loses the people you want to keep
- Hiding it from the team until it becomes a rumor — the rumor is always worse than the truth
- Not raising the issue with the board until it's critical — board members are more useful with more lead time
---
## Crisis 2: Key Person Departure
### Definition
A person whose departure significantly impacts company execution, customer relationships, or team stability. Usually C-level or a critical technical/commercial lead.
### First 24 Hours
- **Clarify what "departure" means.** Resignation? Fired? Mutual agreement? The situation determines the response.
- **Assess the actual impact.** What does this person own that isn't covered? Who on the team will be most affected? Do any customers have primary relationships with this person?
- **Secure institutional knowledge.** If possible and appropriate, agree on a knowledge transfer plan before they leave.
- **Notify the board chair.** Same day. Same rule: facts only, no spin.
- **Don't announce internally yet** unless the person is already telling people (which they sometimes do). Get ahead of it by a few hours if possible.
### First Week
- **Control the narrative internally.** All-hands or department meeting within 2–3 days. Honest: "Name is leaving. Here's what I can share about why. Here's the plan." Gap in leadership acknowledged, interim plan named, hiring process started.
- **Handle customer relationships.** Identify the top 5-10 customers with a relationship with this person. CEO or another senior person reaches out personally. "I want to make sure you hear from me directly..."
- **Announce interim ownership.** Don't leave reporting lines and responsibilities ambiguous. Even a temporary assignment provides stability.
- **Start the search.** Don't wait. The bench is always thinner than you think and searches take 3–4 months.
### Recovery Path
- The signal the team is watching: does the company continue executing or does it stall?
- Keep shipping. Keep hitting targets. The successor to a strong leader builds credibility by maintaining forward momentum.
- Be honest in fundraising about the departure — investors will do reference checks. "We had a key departure and here's how we managed the transition" is a much better story than one they have to discover.
---
## Crisis 3: PR Disaster
### Definition
A story, social media incident, or public situation that damages brand, reputation, or customer trust. Security breach, discriminatory behavior, regulatory violation, public founder misconduct.
### First 24 Hours
- **Establish facts before you communicate.** What actually happened? What data was affected? Who is affected? What is the extent?
- **Activate legal counsel immediately.** Before any external communication. Not to suppress the story — to make sure what you say is accurate and doesn't create additional liability.
- **Designate one spokesperson.** Only one person speaks to media, posts on social. Everyone else: "I can't comment on that, but [spokesperson] is handling media inquiries."
- **Acknowledge, don't stonewall.** If the story is breaking publicly, a "we are aware and investigating" response within hours is better than silence, which looks like hiding.
### First Week
- **Communicate to affected parties first.** If it's a data breach: affected customers before media. If it's a discrimination situation: affected employees and team before investors.
- **Draft a public statement.** Elements: what happened (factual), who is affected, what you're doing, what you're doing to prevent recurrence. No corporate-speak. No deflection. No passive voice ("mistakes were made").
- **Proactively update investors.** They'll hear about it anyway. Hearing from you first, with context, is materially better.
- **Execute the response plan.** Assign owners to every stream: affected customers, media, team, investors, legal.
### Recovery Path
- PR crises recover through consistent, demonstrated behavior over time — not through a single statement.
- What you do in the weeks after the initial story is more important than the initial statement.
- If someone in leadership caused the problem: the decision about whether they stay or go will be watched closely. Protecting the wrong person damages recovery.
- Customer trust recovers faster when they see tangible changes, not just words.
---
## Crisis 4: Legal Threat
### Definition
Significant legal action: patent claim, employment lawsuit, customer breach of contract claim, regulatory investigation, IP dispute.
### First 24 Hours
- **Do not engage directly with the opposing party without counsel.** Nothing — no calls, no emails, no messages.
- **Get legal counsel on the call today.** Not next week. If you have outside counsel, call them. If you don't have a relationship, get one immediately.
- **Document what you know.** The sequence of events, relevant contracts, communications. Don't delete or alter anything — that can become a separate problem.
- **Tell the board chair.** Same day. Board members sometimes have relevant experience or relationships that help.
### First Week
- **Assess exposure.** With counsel: what's the realistic worst case? What's the likely case? What's the cost range?
- **Determine response strategy.** Fight, settle, or ignore (only for clearly frivolous claims with no risk). Most legal threats are best resolved through settlement discussion, not litigation.
- **Evaluate business impact.** Does this affect fundraising? Customer relationships? Employment contracts? Scope the full impact.
- **Communication plan.** Employees? Customers? Investors? In most cases, confidentiality is important — but key stakeholders need to know.
### Recovery Path
- Most legal threats resolve. They resolve faster and cheaper when addressed directly and early.
- Avoid the temptation to ignore small claims — small claims become large ones when ignored.
- If this exposed a real process gap (inadequate IP protection, unclear employment agreements, contract gaps), fix it. The litigation is the signal; the underlying gap is the problem.
---
## Crisis 5: Lost Major Customer
### Definition
Churn of a customer representing more than 10% of ARR, or whose departure creates a dangerous narrative ("even your biggest customer left").
### First 24 Hours
- **Get the real reason.** Not the polite exit reason — the real one. Ask directly: "I want to understand what we could have done differently. Not to change the decision — to learn." Sometimes they'll tell you.
- **Assess financial impact.** Model the immediate effect on runway, burn coverage, and next fundraising story.
- **Notify the board chair.** If this is >10% ARR, same day. No surprises at board meeting.
- **Do not panic-announce internally.** You need a plan before you tell the team.
### First Week
- **Understand the signal.** Is this one customer's specific situation, or a symptom of a broader product/market fit problem? The answer changes the response completely.
- **Address the team.** The team will notice a major logo disappear. Name it, explain what you know, explain what's changing.
- **Accelerate pipeline.** If this creates a gap to target, which deals can be accelerated? What expansion opportunities are there with existing customers?
- **Review other at-risk customers.** Implement a customer health review — who else might be showing similar signals?
### Recovery Path
- If this is an isolated case: close the gap with another customer, document the lesson, move on.
- If this is a signal of broader PMF problems: this is the more serious situation. What are customers getting from you that they can't get elsewhere? Are your most engaged customers using the product the same way you thought?
- The fundraising question: "We lost [major customer]. Why?" Have a direct, honest answer that includes what you changed as a result.
---
## Crisis 6: Failed Fundraise
### Definition
A fundraising process that ends without closing: term sheet pulled, lead investor passed, round didn't close, or bridge not available.
### First 24 Hours
- **Assess actual runway.** How much time do you have at current burn?
- **Identify where the process broke.** Was it valuation? Team? Product? Market? The "why" determines the path.
- **Immediately convene board.** You need their help and their network. A failed raise is not something to manage quietly.
- **Do not tell the team yet.** You need a plan first. "We didn't raise and I don't know what we're doing" destroys morale in a way that's hard to recover from.
### First Week
- **Model survival scenarios.** At current burn: how long? At 50% reduced burn: how long? What does the reduced-burn company look like? Is it sustainable?
- **Identify specific reasons the raise failed.** Investor feedback, even if uncomfortable. "The market doesn't understand our vision" is not useful. "Three investors said the unit economics weren't believable" is useful.
- **Evaluate alternative paths.** Revenue-based financing, venture debt, strategic investment, customer advance payments, bridge from existing investors, acqui-hire.
- **Communicate to team.** Within one week. With a plan. "Here's what we're doing. Here's what this means for the team."
### Recovery Path
- The raise failed for reasons. Fix the reasons. If it was valuation: you may need to lower expectations. If it was market: you may need to refocus. If it was metrics: you need to improve metrics before the next attempt.
- Failed raises are more common than founders discuss publicly. Most companies that eventually succeed have had at least one.
- The companies that recover from failed fundraises usually do so by extending runway aggressively (cutting), finding a lead from outside their normal network, or changing something material about the business.
- **Do not do bridge rounds as avoidance.** A bridge that extends your runway 3 months to a problem you haven't fixed is not a solution. Only bridge if you have a specific, credible path to a successful close.
FILE:references/hard_things.md
# Hard Things — Decision Frameworks for the Calls Nobody Wants to Make
Firing people. Laying off teams. Pivoting when you've raised money on the old direction. Telling a co-founder it's over. Shutting down a product.
This isn't a framework for feeling better about hard calls. It's a framework for making them correctly.
---
## Part 1: Firing
### When to Fire Someone
Most leaders wait too long. By the time they act, everyone else on the team already knows the problem person isn't working out. The team watches the leader, waiting to see if they'll act.
**Fire when:**
- Performance isn't improving after clear, direct, documented feedback
- The person is a culture or values problem, not just a skills problem
- You find yourself routing around them (giving their work to others, excluding them from important discussions)
- The team is being damaged by having them there
- You wouldn't hire them today for this role
**The question to ask:** "If I could wave a magic wand and this person just stopped coming to work, would I be relieved or would I miss them?" If relieved — you already know.
**The hidden test:** "Would I enthusiastically recommend this person to a friend's company for this exact role?" If no, what does that tell you?
### The Warning Signs You're Avoiding the Decision
- You've been "working on it" for more than 3 months
- You're hoping they'll leave on their own
- You're giving them feedback that's softer than what you actually think
- You're planning to "deal with it after the quarter"
- Other team members have started asking you about it
### Before Firing: The Due Diligence
Have you been **direct** — not hinted, not soft-pedaled, but explicitly said "your performance is not meeting the standard required for this role and your job is at risk"?
Have you given them **a fair chance to improve** with clear criteria for what success looks like?
Have you checked whether this is a **fit problem** (wrong role for their skills) vs a **performance problem** (not executing in a role they're capable of)?
Have you considered whether this is **your failure** — bad hire, bad onboarding, bad management — and whether another manager would get different results?
This isn't to talk yourself out of it. It's to make sure you can stand behind the decision.
### How to Fire Someone
**The conversation:**
Do it in person. Start of the week (not Friday — that's cruel). Private meeting. 30 minutes max.
Three sentences:
1. "I have difficult news — today is your last day."
2. "The reason is [one clear sentence — not a list of grievances]."
3. "Here's what the transition looks like [severance, references, timeline]."
**Do not:**
- Soften it so much that the person doesn't understand what's happening
- Give a performance review at the end ("you're really good at X but...")
- Apologize excessively (once is appropriate; more makes it about you)
- Leave open questions about whether this is final (it is)
**The question they'll ask:** "Why now?" Be ready for this. Have a direct answer.
**What to say to the team:** Same day. "I want to let you know that [Name] is no longer with the company. I can't share details, but I want to be transparent that this was a decision we made, not something they chose. Their last day is today." That's it. Don't litigate. Don't share reasons.
### Severance
Be generous. Not because you have to — because it's the right thing to do and it protects the culture. The team watches how you treat people when they leave.
For executives: 2–3 months standard, more if they've been there a long time.
For individual contributors: 2–4 weeks per year of service is reasonable.
**Reference:** Only confirm dates and title (standard practice). If you genuinely believe they'd be good somewhere else, offer a more substantive reference. Don't damage their career because the fit wasn't right.
---
## Part 2: Layoffs
### The First Question: Is This the Right Move?
Layoffs are sometimes the right call. But they're also sometimes an avoidance tactic — avoiding harder decisions about business model, spending discipline, or strategic direction.
Before proceeding, be clear on what problem you're solving:
- **Extending runway:** How many months does this buy? Is that enough?
- **Restructuring:** Are you changing the direction of the company, not just the headcount?
- **Cost cutting without strategic change:** This is usually a mistake — you lose talent, damage culture, and face the same problem 6 months later.
**The math:** At your current burn, you need to cut \_\_% to extend runway from \_\_ months to \_\_ months. That math should drive the decision, not a "feels about right" number.
### Cut Once, Cut Deep
The worst outcome is two rounds of layoffs. After the first, the people who stay are already thinking about leaving. A second round converts "scared" to "gone."
If you're going to do this, do it once and do it to a level that solves the problem for 18+ months. Psychological safety matters more than any individual cost saving.
### Deciding Who to Let Go
This is the hardest part. A framework:
**By role:** Does the company need this function at current stage? If you're cutting a whole team or capability, it's cleaner, more defensible, and recovers faster.
**By performance:** If cutting across teams, higher performers stay. This is the moment where the "we have no B players" culture claim is tested.
**By span of work:** Which work is critical path to the strategy you're executing now? Everything else is a candidate.
**The veto question:** "Would I fight to keep this person if they said they were leaving?" If yes, they're safe. If no, they're a candidate.
### The Layoff Conversation
**Preparation:**
- Legal review first. In Germany: Betriebsrat, social selection, proper notice periods. In the US: WARN Act for 50+ employees. Do not skip this.
- Have severance paperwork ready before the conversation
- Have IT ready to revoke access (dignity: after the conversation, not during)
**The conversation:**
- Private. Direct.
- "We're restructuring the company and your role is being eliminated."
- Don't blame the person. Don't say "we had to make hard choices" three times. Say it once and move on.
- Explain severance, timeline, references clearly.
- Answer questions. "I don't know" is acceptable for some questions. "I can't tell you" is not.
**All-hands same day:**
- You, live, as soon as individual conversations are done
- Be honest about why and what it means for the company
- Answer hard questions. Don't hide behind PR language.
- Acknowledge that this is hard and that you're responsible for the decisions that led here
### Survivor Guilt
The people who didn't get cut will feel: relieved, guilty, scared, and angry — often all four. Don't underestimate this.
Within 48 hours of the layoff:
- Talk to every team lead individually
- Hold a team meeting for each department
- Be available for hard conversations
The question everyone is silently asking: "Am I next?" Answer it directly, even if you can't promise the future: "I don't plan any further cuts. Here's what would have to be true for that to change."
---
## Part 3: Pivoting
### Signals That It's Time to Pivot
- Product-market fit isn't materializing despite iteration
- Growth requires heroic sales effort on every deal
- The customers who love you are not the customers you expected
- You find a problem you can solve well that's adjacent to what you're doing
- The market you targeted is smaller than you thought
**The danger signal:** You're pivoting to run from failure, not toward opportunity. Pivots pulled by evidence of a better path work. Pivots pushed by exhaustion with the current path fail differently.
### How to Think About the Pivot
Define what you're keeping vs. what you're changing:
- **Team**: usually keeping — the team is the asset
- **Technology**: partially keeping — usually can be reoriented
- **Customers**: depends — some will follow, some won't
- **Vision**: the long-term vision often survives; the near-term path changes
- **Brand**: sometimes requires a rename
The cleanest pivots have a clear answer to: "Why are we better positioned to win at the new thing than anyone else?"
### Telling the Board You're Pivoting
Do not surprise the board in a board meeting. Have the conversation individually with key directors first.
What to communicate:
1. What changed — the new data or insight that's driving this
2. What you're moving away from and why
3. What you're moving to and why you can win there
4. What this means for fundraising timeline and strategy
5. What you need from them
Board members hate two things: surprises and not being consulted. Give them both the information and the opportunity to contribute.
### Telling Customers You're Pivoting
Be direct. Don't spin it as "we're expanding our focus." If you're killing something they use, tell them clearly, with enough notice for them to plan.
What customers need to know:
- What's changing and when
- What happens to their data / integrations / workflows
- Who their contact is through the transition
- What alternatives exist
Customers who feel respected through a hard change sometimes become your biggest advocates. Customers who feel deceived become your loudest critics.
---
## Part 4: Co-Founder Conflicts
### The Types of Conflict
**Values/direction conflict:** You disagree fundamentally about what the company should be. This is existential and usually doesn't resolve with more conversation.
**Performance conflict:** One co-founder isn't pulling their weight. This is hard but more tractable — it's addressable with clarity.
**Role/scope conflict:** Unclear ownership causing friction. This is often fixable.
### The Conversation You're Not Having
Most co-founder conflicts fester because nobody says the real thing out loud.
The real thing might be: "I don't think you're growing into what this company needs." Or: "I don't agree with the direction you're pushing us and I don't feel heard." Or: "I'm doing 70% of the work and we have equal equity."
Say the real thing. Not in anger. Clearly, directly, with respect.
### When It's Not Working
Signs the co-founder relationship is unsalvageable:
- You've had the real conversation and nothing changed
- You don't trust their judgment anymore
- You've stopped including them in important decisions
- You're telling people (investors, team) a different story than what's true
- The team has started choosing sides
### The Separation
Options in rough order of impact:
1. **Role change** — they move to a different function where they can succeed
2. **Advisor role** — they step out of operations, keep some equity, maintain relationship
3. **Full exit** — they leave the company
For any separation: legal counsel first. Cap table, vesting, IP assignment, competition clauses — all need to be addressed. Don't make handshake deals.
How you treat the departing co-founder tells the team, the investors, and the market who you are.
---
## Part 5: Shutting Down a Product Line
### When to Kill It
- Revenue doesn't justify the cost (including the opportunity cost of what the team could be building instead)
- It's pulling the company in a strategic direction you're not committed to
- It requires resources disproportionate to its potential
- Supporting it is making the rest of the product worse
**The question to ask:** "If we launched this today knowing what we know, would we build it?" If no, that's your answer.
### What You're Protecting
The customers who use it. They trusted you with their workflow. Give them:
- Clear timeline (90 days minimum for anything with integration dependencies)
- Migration path to alternatives or your other products
- Data export
- A person they can contact with questions
### Internal Communication
The team that built it feels the loss personally. Acknowledge it. "This product represents real work and real care. Shutting it down is not a judgment of the team — it's a judgment about fit with where the company is going."
If team members are being reassigned, not let go — make that clear immediately. The fear of job loss will dominate every other concern until you address it.
FILE:scripts/decision_matrix_scorer.py
#!/usr/bin/env python3
"""
Decision Matrix Scorer — Executive Mentor Tool
Weighted multi-criteria decision analysis with sensitivity testing.
Answers: Which option wins? How fragile is that result? Where are the close calls?
Usage:
python decision_matrix_scorer.py # Run with sample data
python decision_matrix_scorer.py --interactive # Interactive mode
python decision_matrix_scorer.py --file data.json # Load from JSON file
JSON format:
{
"decision": "Description of the decision",
"criteria": [
{"name": "Criterion Name", "weight": 0.3, "description": "Optional"},
...
],
"options": [
{
"name": "Option Name",
"description": "Optional description",
"scores": {"Criterion Name": 8, "Another": 6, ...}
},
...
]
}
Scores: 1–10 scale. Weights: must sum to 1.0 (or will be normalized).
"""
import json
import sys
import argparse
from typing import List, Dict, Tuple
# ─────────────────────────────────────────────────────
# Core data structures
# ─────────────────────────────────────────────────────
def normalize_weights(criteria: List[Dict]) -> List[Dict]:
"""Ensure weights sum to 1.0."""
total = sum(c["weight"] for c in criteria)
if abs(total - 1.0) > 0.001:
for c in criteria:
c["weight"] = c["weight"] / total
return criteria
def score_option(option: Dict, criteria: List[Dict]) -> float:
"""Calculate weighted score for an option."""
total = 0.0
for c in criteria:
score = option["scores"].get(c["name"], 5) # Default to 5 if missing
total += score * c["weight"]
return round(total, 3)
def score_all(options: List[Dict], criteria: List[Dict]) -> List[Tuple[str, float]]:
"""Return sorted list of (option_name, weighted_score)."""
results = []
for opt in options:
s = score_option(opt, criteria)
results.append((opt["name"], s))
return sorted(results, key=lambda x: x[1], reverse=True)
# ─────────────────────────────────────────────────────
# Sensitivity analysis
# ─────────────────────────────────────────────────────
def sensitivity_analysis(options: List[Dict], criteria: List[Dict]) -> Dict:
"""
Test how result changes when each criterion's weight is varied ±30%.
Returns dict: criterion → {stable: bool, risk_of_flip: bool, details: str}
"""
baseline = score_all(options, criteria)
winner = baseline[0][0]
results = {}
for i, c in enumerate(criteria):
flips = []
for delta in [-0.30, -0.20, -0.10, +0.10, +0.20, +0.30]:
# Adjust weight of criterion i, redistribute remainder proportionally
test_criteria = [dict(cr) for cr in criteria]
new_weight = max(0.01, test_criteria[i]["weight"] + delta)
old_weight = test_criteria[i]["weight"]
diff = new_weight - old_weight
# Redistribute diff across other criteria
others = [j for j in range(len(test_criteria)) if j != i]
total_other = sum(test_criteria[j]["weight"] for j in others)
if total_other > 0:
for j in others:
proportion = test_criteria[j]["weight"] / total_other
test_criteria[j]["weight"] -= diff * proportion
test_criteria[j]["weight"] = max(0.01, test_criteria[j]["weight"])
test_criteria[i]["weight"] = new_weight
test_criteria = normalize_weights(test_criteria)
test_results = score_all(options, test_criteria)
if test_results[0][0] != winner:
flips.append((delta, test_results[0][0]))
if flips:
smallest_delta = min(abs(delta) for delta, _name in flips)
results[c["name"]] = {
"stable": False,
"flip_at": f"±{int(smallest_delta*100)}% weight change",
"flip_to": flips[0][1],
"importance": "HIGH — result depends heavily on this weight"
}
else:
results[c["name"]] = {
"stable": True,
"flip_at": None,
"flip_to": None,
"importance": "LOW — winner holds even with significant weight changes"
}
return results
def close_call_analysis(results: List[Tuple[str, float]]) -> List[Dict]:
"""Find options within 10% of winner score — these are close calls."""
if not results:
return []
winner_score = results[0][1]
close = []
for name, score in results[1:]:
gap = winner_score - score
gap_pct = (gap / winner_score * 100) if winner_score > 0 else 0
if gap_pct <= 15:
close.append({
"name": name,
"score": score,
"gap": round(gap, 3),
"gap_pct": round(gap_pct, 1),
"verdict": "Very close — recheck assumptions" if gap_pct <= 5 else "Close — worth a second look"
})
return close
def criterion_breakdown(options: List[Dict], criteria: List[Dict]) -> Dict:
"""Show per-criterion scores for each option."""
breakdown = {}
for opt in options:
breakdown[opt["name"]] = {}
for c in criteria:
raw = opt["scores"].get(c["name"], 5)
weighted = raw * c["weight"]
breakdown[opt["name"]][c["name"]] = {
"raw": raw,
"weighted": round(weighted, 3),
"weight": f"{round(c['weight']*100)}%"
}
return breakdown
# ─────────────────────────────────────────────────────
# Output formatting
# ─────────────────────────────────────────────────────
def hr(char="─", width=65):
return char * width
def print_report(data: Dict):
"""Print the full decision analysis report."""
decision = data.get("decision", "Unnamed Decision")
criteria = normalize_weights(data["criteria"])
options = data["options"]
print()
print(hr("═"))
print(f" DECISION MATRIX ANALYSIS")
print(f" {decision}")
print(hr("═"))
# ── Criteria summary
print()
print("CRITERIA & WEIGHTS")
print(hr())
for c in sorted(criteria, key=lambda x: x["weight"], reverse=True):
bar_len = int(c["weight"] * 30)
bar = "█" * bar_len
desc = f" — {c['description']}" if c.get("description") else ""
print(f" {c['name']:<25} {c['weight']*100:>5.1f}% {bar}{desc}")
# ── Scoring results
print()
print("RESULTS (ranked)")
print(hr())
results = score_all(options, criteria)
max_score = 10.0 # max possible weighted score
for rank, (name, score) in enumerate(results, 1):
pct = score / 10.0
bar_len = int(pct * 40)
bar = "█" * bar_len
medal = ["🥇", "🥈", "🥉"][rank-1] if rank <= 3 else f"#{rank} "
print(f" {medal} {name:<25} {score:>5.2f}/10 {bar}")
winner = results[0][0]
print()
print(f" ► Winner: {winner} (score: {results[0][1]:.2f})")
# ── Close calls
close = close_call_analysis(results)
if close:
print()
print("CLOSE CALLS")
print(hr())
for c in close:
print(f" ⚠ {c['name']}: {c['score']:.2f} (gap: {c['gap_pct']}% — {c['verdict']})")
# ── Per-criterion breakdown
print()
print("SCORE BREAKDOWN BY CRITERION")
print(hr())
breakdown = criterion_breakdown(options, criteria)
# Header
opt_names = [opt["name"][:16] for opt in options]
header = f" {'Criterion':<22}"
for n in opt_names:
header += f" {n:>10}"
print(header)
print(" " + hr("-", 63))
for c in criteria:
row = f" {c['name']:<22}"
for opt in options:
raw = opt["scores"].get(c["name"], 5)
row += f" {raw:>10}"
row += f" (weight {c['weight']*100:.0f}%)"
print(row)
# Weighted row
print(" " + hr("-", 63))
weighted_row = f" {'Weighted Total':<22}"
for name, score in results:
# Re-order by options list order
weighted_row += f" {score:>10.2f}"
# Actually print in options order
print(f" {'Weighted Total':<22}", end="")
for opt in options:
s = score_option(opt, criteria)
print(f" {s:>10.2f}", end="")
print()
# ── Sensitivity analysis
print()
print("SENSITIVITY ANALYSIS")
print(hr())
print(" How much does the winner change if we adjust criterion weights?")
print()
sensitivity = sensitivity_analysis(options, criteria)
for crit_name, result in sensitivity.items():
if result["stable"]:
print(f" ✓ {crit_name:<28} STABLE — winner holds at ±30% weight change")
else:
print(f" ⚠ {crit_name:<28} FRAGILE — flips to '{result['flip_to']}' at {result['flip_at']}")
# ── Recommendation
print()
print("RECOMMENDATION")
print(hr())
unstable = [k for k, v in sensitivity.items() if not v["stable"]]
if unstable:
print(f" Winner: {winner}")
print(f" Confidence: MEDIUM — result is sensitive to weights on: {', '.join(unstable)}")
print()
print(" Before committing:")
print(f" • Validate that your weighting of [{', '.join(unstable)}] is correct")
print(" • Consider whether the weight differences reflect genuine priorities")
print(" • If uncertain, run scenario with alternative weights")
else:
print(f" Winner: {winner}")
print(f" Confidence: HIGH — winner is stable across all weight scenarios")
print()
print(" The decision is clear. The main risk is whether your scoring")
print(" of each option on each criterion is accurate.")
print()
print(hr("═"))
print()
# ─────────────────────────────────────────────────────
# Interactive mode
# ─────────────────────────────────────────────────────
def interactive_mode():
"""Guided interactive data entry."""
print()
print(hr("═"))
print(" DECISION MATRIX — Interactive Mode")
print(hr("═"))
data = {}
data["decision"] = input("\nWhat decision are you making?\n> ").strip()
# Criteria
print("\nDefine criteria (what matters in this decision).")
print("Enter criteria one at a time. Empty line to finish.")
print("Weight: importance 0–10 (will be normalized to %).")
print()
criteria = []
while True:
name = input(f"Criterion {len(criteria)+1} name (or ENTER to finish): ").strip()
if not name:
if len(criteria) < 2:
print(" Need at least 2 criteria.")
continue
break
weight_str = input(f" Weight for '{name}' (0–10): ").strip()
try:
weight = float(weight_str)
except ValueError:
weight = 5.0
criteria.append({"name": name, "weight": weight})
data["criteria"] = criteria
# Options
print("\nDefine options (what you're choosing between).")
print("Enter options one at a time. Empty line to finish.")
print()
options = []
while True:
name = input(f"Option {len(options)+1} name (or ENTER to finish): ").strip()
if not name:
if len(options) < 2:
print(" Need at least 2 options.")
continue
break
print(f"\n Score each criterion for '{name}' (1=poor, 10=excellent):")
scores = {}
for c in criteria:
while True:
s = input(f" {c['name']}: ").strip()
try:
score = float(s)
if 1 <= score <= 10:
scores[c["name"]] = score
break
else:
print(" Score must be 1–10")
except ValueError:
print(" Enter a number 1–10")
options.append({"name": name, "scores": scores})
print()
data["options"] = options
print_report(data)
# ─────────────────────────────────────────────────────
# Sample data
# ─────────────────────────────────────────────────────
SAMPLE_DATA = {
"decision": "How to extend runway: Cut costs vs. Raise bridge vs. Accelerate revenue",
"criteria": [
{
"name": "Speed to impact",
"weight": 0.25,
"description": "How quickly does this improve our situation?"
},
{
"name": "Execution risk",
"weight": 0.30,
"description": "How likely is this to actually work? (10=low risk)"
},
{
"name": "Team morale impact",
"weight": 0.20,
"description": "Effect on team (10=positive, 1=very negative)"
},
{
"name": "Runway extension",
"weight": 0.15,
"description": "How much runway does this actually buy?"
},
{
"name": "Strategic fit",
"weight": 0.10,
"description": "Does this align with where we want to go?"
}
],
"options": [
{
"name": "Cost cut 25%",
"description": "Reduce headcount and discretionary spend by 25%",
"scores": {
"Speed to impact": 9,
"Execution risk": 8,
"Team morale impact": 2,
"Runway extension": 8,
"Strategic fit": 5
}
},
{
"name": "Bridge from investors",
"description": "Raise $500K bridge from existing investors to hit next milestone",
"scores": {
"Speed to impact": 6,
"Execution risk": 5,
"Team morale impact": 7,
"Runway extension": 6,
"Strategic fit": 7
}
},
{
"name": "Accelerate revenue",
"description": "Push 3 enterprise deals hard, offer incentives for Q4 close",
"scores": {
"Speed to impact": 4,
"Execution risk": 3,
"Team morale impact": 9,
"Runway extension": 9,
"Strategic fit": 10
}
},
{
"name": "Hybrid: cut 15% + bridge",
"description": "Smaller cuts combined with a modest bridge round",
"scores": {
"Speed to impact": 7,
"Execution risk": 6,
"Team morale impact": 5,
"Runway extension": 7,
"Strategic fit": 6
}
}
]
}
# ─────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Decision Matrix Scorer — weighted analysis with sensitivity testing"
)
parser.add_argument(
"--interactive", "-i",
action="store_true",
help="Interactive mode: enter decision data manually"
)
parser.add_argument(
"--file", "-f",
type=str,
help="Load decision data from JSON file"
)
parser.add_argument(
"--sample",
action="store_true",
help="Show sample data structure and exit"
)
args = parser.parse_args()
if args.sample:
print(json.dumps(SAMPLE_DATA, indent=2))
return
if args.interactive:
interactive_mode()
return
if args.file:
try:
with open(args.file) as f:
data = json.load(f)
print_report(data)
except FileNotFoundError:
print(f"Error: File '{args.file}' not found.")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.file}': {e}")
sys.exit(1)
return
# Default: run sample data
print()
print("Running with sample data. Use --interactive for custom input or --file for JSON.")
print_report(SAMPLE_DATA)
if __name__ == "__main__":
main()
FILE:scripts/stakeholder_mapper.py
#!/usr/bin/env python3
"""
Stakeholder Mapper — Executive Mentor Tool
Maps stakeholders by influence and alignment.
Identifies: champions, blockers, swing votes, and hidden risks.
Outputs: stakeholder grid with engagement strategy per quadrant.
Usage:
python stakeholder_mapper.py # Run with sample data
python stakeholder_mapper.py --interactive # Interactive mode
python stakeholder_mapper.py --file data.json # Load from JSON file
JSON format:
{
"initiative": "Name of the decision or initiative",
"stakeholders": [
{
"name": "Person/Group Name",
"role": "Their role or title",
"influence": 8, // 1–10: how much power they have over outcome
"alignment": 3, // 1–10: how supportive they are (10=champion, 1=blocker)
"interest": 7, // 1–10: how interested/engaged they are
"notes": "Optional context — what drives them, hidden concerns, relationships"
}
]
}
"""
import json
import sys
import argparse
from typing import List, Dict, Tuple, Optional
# ─────────────────────────────────────────────────────
# Quadrant classification
# ─────────────────────────────────────────────────────
def classify_stakeholder(influence: float, alignment: float) -> Dict:
"""
Classify into strategic quadrant based on influence and alignment.
Quadrants:
- Champions (high influence, high alignment): Your most valuable assets
- Blockers (high influence, low alignment): Your biggest risks
- Supporters (low influence, high alignment): Useful but less critical
- Bystanders (low influence, low alignment): Monitor, low priority
- Swing Votes (medium influence, medium alignment): Key to persuade
"""
mid_influence = 5.5
mid_alignment = 5.5
# Special case: swing votes — medium on both dimensions
if 4 <= influence <= 7 and 4 <= alignment <= 7:
return {
"quadrant": "Swing Vote",
"symbol": "⚡",
"priority": "HIGH",
"strategy": "Persuade — understand concerns, address directly, build relationship"
}
if influence >= mid_influence and alignment >= mid_alignment:
return {
"quadrant": "Champion",
"symbol": "★",
"priority": "HIGH",
"strategy": "Leverage — activate them as advocates, give them a role in the initiative"
}
elif influence >= mid_influence and alignment < mid_alignment:
return {
"quadrant": "Blocker",
"symbol": "✖",
"priority": "CRITICAL",
"strategy": "Address — understand their specific objections, find common ground or neutralize"
}
elif influence < mid_influence and alignment >= mid_alignment:
return {
"quadrant": "Supporter",
"symbol": "○",
"priority": "MEDIUM",
"strategy": "Maintain — keep informed and engaged, potentially increase their influence"
}
else:
return {
"quadrant": "Bystander",
"symbol": "·",
"priority": "LOW",
"strategy": "Monitor — minimal investment, keep informed with standard comms"
}
def risk_flags(stakeholder: Dict) -> List[str]:
"""Identify specific risk signals for a stakeholder."""
flags = []
influence = stakeholder["influence"]
alignment = stakeholder["alignment"]
interest = stakeholder.get("interest", 5)
if influence >= 7 and alignment <= 3:
flags.append("🔴 HIGH-POWER BLOCKER — can kill this initiative")
if influence >= 7 and alignment <= 5 and interest >= 7:
flags.append("🟡 ENGAGED SKEPTIC — high influence, paying close attention, not convinced")
if alignment <= 4 and interest >= 8:
flags.append("🟡 ACTIVE OPPOSITION — low alignment but highly engaged — may mobilize others")
if influence >= 6 and alignment >= 7 and interest <= 3:
flags.append("🟡 DISENGAGED CHAMPION — strong supporter but not paying attention — needs activation")
if influence >= 5 and 4 <= alignment <= 6:
flags.append("⚡ PERSUADABLE — medium influence, genuinely undecided — high ROI to engage")
return flags
# ─────────────────────────────────────────────────────
# Analysis
# ─────────────────────────────────────────────────────
def calculate_overall_alignment(stakeholders: List[Dict]) -> Dict:
"""Calculate weighted average alignment (weighted by influence)."""
if not stakeholders:
return {"score": 0, "verdict": "No data"}
total_influence = sum(s["influence"] for s in stakeholders)
if total_influence == 0:
return {"score": 0, "verdict": "No influence"}
weighted_alignment = sum(
s["alignment"] * s["influence"] for s in stakeholders
) / total_influence
if weighted_alignment >= 7:
verdict = "FAVORABLE — strong support among influential stakeholders"
elif weighted_alignment >= 5:
verdict = "MIXED — significant opposition needs to be addressed"
else:
verdict = "UNFAVORABLE — initiative faces significant headwinds"
return {
"score": round(weighted_alignment, 2),
"verdict": verdict
}
def find_critical_path(stakeholders: List[Dict]) -> List[Dict]:
"""
Identify the minimal set of stakeholders whose alignment is critical.
These are high-influence stakeholders — their position determines the outcome.
"""
high_influence = [s for s in stakeholders if s["influence"] >= 7]
return sorted(high_influence, key=lambda x: x["influence"], reverse=True)
def engagement_sequencing(stakeholders: List[Dict]) -> List[Dict]:
"""
Recommend engagement sequence.
Order: Fix blockers → Activate champions → Persuade swing votes → Maintain rest.
"""
classified = []
for s in stakeholders:
cls = classify_stakeholder(s["influence"], s["alignment"])
classified.append({**s, **cls})
# Sort by engagement priority
priority_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
classified.sort(key=lambda x: (priority_order[x["priority"]], -x["influence"]))
return classified
# ─────────────────────────────────────────────────────
# ASCII grid visualization
# ─────────────────────────────────────────────────────
def render_grid(stakeholders: List[Dict], width: int = 60) -> str:
"""
Render a 2D influence vs alignment grid with stakeholder positions.
Y-axis: Influence (top = high)
X-axis: Alignment (left = low, right = high)
"""
rows = 10
cols = 20
grid = [[' ' for _ in range(cols)] for _ in range(rows)]
for s in stakeholders:
influence = s["influence"]
alignment = s["alignment"]
# Map scores 1–10 to grid coordinates
col = int((alignment - 1) / 9 * (cols - 1))
row = rows - 1 - int((influence - 1) / 9 * (rows - 1))
col = max(0, min(cols - 1, col))
row = max(0, min(rows - 1, row))
initial = s["name"][0].upper()
if grid[row][col] == ' ':
grid[row][col] = initial
else:
grid[row][col] = '+' # Overlap
lines = []
lines.append(" STAKEHOLDER MAP (Influence ↑ | Alignment →)")
lines.append("")
lines.append(f" HIGH ┌{'─'*cols}┐")
for i, row in enumerate(grid):
if i == rows // 2:
prefix = " INFL "
else:
prefix = " "
lines.append(f"{prefix}│{''.join(row)}│")
lines.append(f" LOW └{'─'*cols}┘")
lines.append(f" {'BLOCKER':<12} {'SWING':<8} CHAMPION")
lines.append(f" Low alignment High alignment")
lines.append("")
# Legend
lines.append(" Legend (initials):")
for s in stakeholders:
cls = classify_stakeholder(s["influence"], s["alignment"])
lines.append(f" {s['name'][0].upper()} = {s['name']} ({cls['symbol']} {cls['quadrant']})")
return "\n".join(lines)
# ─────────────────────────────────────────────────────
# Output formatting
# ─────────────────────────────────────────────────────
def hr(char="─", width=65):
return char * width
def print_report(data: Dict):
initiative = data.get("initiative", "Unnamed Initiative")
stakeholders = data["stakeholders"]
# Validate and fill defaults
for s in stakeholders:
s.setdefault("interest", 5)
s.setdefault("notes", "")
s["influence"] = max(1, min(10, float(s["influence"])))
s["alignment"] = max(1, min(10, float(s["alignment"])))
s["interest"] = max(1, min(10, float(s["interest"])))
print()
print(hr("═"))
print(f" STAKEHOLDER ANALYSIS")
print(f" {initiative}")
print(hr("═"))
# Overall assessment
overall = calculate_overall_alignment(stakeholders)
print()
print("OVERALL ASSESSMENT")
print(hr())
print(f" Weighted alignment score: {overall['score']}/10")
print(f" Verdict: {overall['verdict']}")
# Grid visualization
print()
print(hr())
print(render_grid(stakeholders))
# Stakeholder profiles by quadrant
sequenced = engagement_sequencing(stakeholders)
# Group by quadrant
quadrants = {}
for s in sequenced:
q = s["quadrant"]
if q not in quadrants:
quadrants[q] = []
quadrants[q].append(s)
quadrant_order = ["Blocker", "Swing Vote", "Champion", "Supporter", "Bystander"]
print()
print("STAKEHOLDER PROFILES")
print(hr())
for q_name in quadrant_order:
if q_name not in quadrants:
continue
group = quadrants[q_name]
first = group[0]
print()
print(f" {first['symbol']} {q_name.upper()}S ({len(group)} stakeholder{'s' if len(group)>1 else ''})")
print(f" Strategy: {first['strategy']}")
print()
for s in group:
cls = classify_stakeholder(s["influence"], s["alignment"])
flags = risk_flags(s)
print(f" {s['name']}")
print(f" Role: {s.get('role', 'Not specified')}")
print(f" Influence: {'█'*int(s['influence']//2)}{'░'*(5-int(s['influence']//2))} {s['influence']:.0f}/10 "
f"Alignment: {'█'*int(s['alignment']//2)}{'░'*(5-int(s['alignment']//2))} {s['alignment']:.0f}/10 "
f"Interest: {'█'*int(s['interest']//2)}{'░'*(5-int(s['interest']//2))} {s['interest']:.0f}/10")
if flags:
for flag in flags:
print(f" {flag}")
if s.get("notes"):
print(f" Notes: {s['notes']}")
print()
# Engagement plan
print()
print("ENGAGEMENT PLAN (sequenced by priority)")
print(hr())
print()
print(f" {'#':<3} {'Name':<22} {'Quadrant':<14} {'Priority':<10} {'First Action'}")
print(f" {hr('-', 63)}")
actions = {
"Blocker": "Schedule 1:1 — understand specific objections",
"Swing Vote": "Coffee or informal conversation — listen first",
"Champion": "Brief them on the initiative — give them a role",
"Supporter": "Keep informed — monthly update or email",
"Bystander": "Include in standard comms only"
}
for i, s in enumerate(sequenced, 1):
action = actions.get(s["quadrant"], "Maintain standard communication")
print(f" {i:<3} {s['name']:<22} {s['quadrant']:<14} {s['priority']:<10} {action}")
# Risk summary
print()
print("RISK SUMMARY")
print(hr())
critical_path = find_critical_path(stakeholders)
if critical_path:
print()
print(" High-influence stakeholders (outcome depends on these):")
for s in critical_path:
cls = classify_stakeholder(s["influence"], s["alignment"])
alignment_label = "CHAMPION" if s["alignment"] >= 7 else "BLOCKER" if s["alignment"] <= 4 else "UNDECIDED"
print(f" {cls['symbol']} {s['name']:<25} influence {s['influence']:.0f}/10 → {alignment_label}")
# All risk flags
all_flags = []
for s in stakeholders:
flags = risk_flags(s)
for flag in flags:
all_flags.append((s["name"], flag))
if all_flags:
print()
print(" Risk flags:")
for name, flag in all_flags:
print(f" [{name}] {flag}")
print()
print(hr("═"))
print()
# ─────────────────────────────────────────────────────
# Interactive mode
# ─────────────────────────────────────────────────────
def interactive_mode():
print()
print(hr("═"))
print(" STAKEHOLDER MAPPER — Interactive Mode")
print(hr("═"))
data = {}
data["initiative"] = input("\nWhat initiative or decision are you mapping?\n> ").strip()
print("\nAdd stakeholders one at a time. Empty name to finish.")
print("Scores: 1=low, 10=high")
print()
stakeholders = []
while True:
name = input(f"Stakeholder {len(stakeholders)+1} name (or ENTER to finish): ").strip()
if not name:
if len(stakeholders) < 1:
print(" Need at least 1 stakeholder.")
continue
break
role = input(f" Role/title: ").strip()
def get_score(prompt, default=5):
while True:
s = input(f" {prompt} (1–10, default {default}): ").strip()
if not s:
return float(default)
try:
v = float(s)
if 1 <= v <= 10:
return v
print(" Must be 1–10")
except ValueError:
print(" Enter a number")
influence = get_score("Influence (power over this decision)")
alignment = get_score("Alignment (1=opposed, 10=champion)")
interest = get_score("Interest level (how engaged are they)")
notes = input(f" Notes (optional): ").strip()
stakeholders.append({
"name": name,
"role": role,
"influence": influence,
"alignment": alignment,
"interest": interest,
"notes": notes
})
print()
data["stakeholders"] = stakeholders
print_report(data)
# ─────────────────────────────────────────────────────
# Sample data
# ─────────────────────────────────────────────────────
SAMPLE_DATA = {
"initiative": "Migrate from monolith to microservices (18-month program)",
"stakeholders": [
{
"name": "Sarah Chen (CTO)",
"role": "Chief Technology Officer",
"influence": 10,
"alignment": 9,
"interest": 9,
"notes": "Driving force behind the initiative. Will fund and protect the team."
},
{
"name": "Marcus Webb (CFO)",
"role": "Chief Financial Officer",
"influence": 9,
"alignment": 3,
"interest": 6,
"notes": "Concerned about 18-month cost with no visible revenue return. Has budget veto."
},
{
"name": "Priya Agarwal (VP Eng)",
"role": "VP Engineering",
"influence": 8,
"alignment": 7,
"interest": 8,
"notes": "Supportive in principle, worried about team bandwidth alongside feature delivery."
},
{
"name": "Tom Briggs (VP Product)",
"role": "VP Product",
"influence": 7,
"alignment": 4,
"interest": 5,
"notes": "Concerned about roadmap slowdown. Hasn't been in the architecture discussions."
},
{
"name": "Elena Park (CEO)",
"role": "Chief Executive Officer",
"influence": 10,
"alignment": 6,
"interest": 4,
"notes": "Trusts the CTO but will back out if CFO and VP Product both push back hard."
},
{
"name": "Raj Patel (Lead Arch)",
"role": "Lead Architect",
"influence": 6,
"alignment": 10,
"interest": 10,
"notes": "Deep technical champion. Has proposed detailed migration plan."
},
{
"name": "Dev Team Leads (x4)",
"role": "Team Leads",
"influence": 5,
"alignment": 6,
"interest": 7,
"notes": "Mixed. Some excited, some worried about learning curve. Middle ground."
},
{
"name": "Board (investor reps)",
"role": "Board Directors",
"influence": 9,
"alignment": 5,
"interest": 3,
"notes": "Not paying attention unless CFO raises flags. Could become blockers if CFO escalates."
}
]
}
# ─────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Stakeholder Mapper — influence, alignment, and engagement strategy"
)
parser.add_argument(
"--interactive", "-i",
action="store_true",
help="Interactive mode: enter stakeholder data manually"
)
parser.add_argument(
"--file", "-f",
type=str,
help="Load stakeholder data from JSON file"
)
parser.add_argument(
"--sample",
action="store_true",
help="Print sample JSON structure and exit"
)
args = parser.parse_args()
if args.sample:
print(json.dumps(SAMPLE_DATA, indent=2))
return
if args.interactive:
interactive_mode()
return
if args.file:
try:
with open(args.file) as f:
data = json.load(f)
print_report(data)
except FileNotFoundError:
print(f"Error: File '{args.file}' not found.")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.file}': {e}")
sys.exit(1)
return
# Default: sample data
print()
print("Running with sample data. Use --interactive for custom input or --file for JSON.")
print_report(SAMPLE_DATA)
if __name__ == "__main__":
main()
FILE:skills/board-prep/SKILL.md
---
name: "board-prep"
description: "/em -board-prep — Board Meeting Preparation"
---
# /em:board-prep — Board Meeting Preparation
**Command:** `/em:board-prep <agenda>`
Prepare for the adversarial version of your board, not the friendly one. Every hard question they'll ask. Every number you need cold. The narrative that acknowledges weakness without losing the room.
---
## The Reality of Board Meetings
Your board members have seen 50+ companies. They've watched founders flinch at their own numbers, spin bad news as "learning opportunities," and present sanitized decks that hide what's actually happening.
They know when you're not being straight with them. The question isn't whether they'll ask the hard questions — it's whether you're ready for them.
The best board meetings aren't the ones where everything looks good. They're the ones where the CEO demonstrates they see reality clearly, have a plan, and can execute under pressure.
---
## The Preparation Framework
### Phase 1: Numbers Cold
Before the meeting, every number in your deck should live in your head, not just the slide.
**The numbers you must know without looking:**
- Current MRR / ARR and month-over-month growth rate
- Burn rate (monthly) and runway (months at current burn)
- Headcount by department
- CAC and LTV by channel / segment
- Net Revenue Retention
- Pipeline: value, conversion rate, average sales cycle
- Churn: rate, top reasons, top churned accounts
- Gross margin (product), net margin (company)
- Key hiring positions open and time-to-fill
**Stress test yourself:** Can you answer "what's your burn?" without hesitation? "What's your churn rate by segment?" If you pause, you don't know it.
### Phase 2: Anticipate the Hard Questions
For every item on the agenda, generate the adversarial version of the question.
**Standard adversarial questions by topic:**
*Revenue performance:*
- "You missed revenue by 20% this quarter. What specifically failed?"
- "Is this a pipeline problem, a conversion problem, or a capacity problem?"
- "If you missed because of one big deal, how dependent is your model on individual deals?"
- "When do you project recovery and what are the leading indicators you're right?"
*Runway / burn:*
- "At current burn you have N months. What's your plan if the next round takes 9 months?"
- "What would you cut first if you had to extend runway by 6 months today?"
- "Is there a scenario where you don't raise another round?"
*Product / roadmap:*
- "You shipped X. What did customers actually do with it?"
- "What did you kill this quarter and why?"
- "Where are you behind on roadmap? What's slipping?"
*Team:*
- "Who's at risk of leaving? How would that affect execution?"
- "You've had 3 VP-level hires not work out. What pattern do you see?"
- "Is the team the right team for this stage?"
*Competition:*
- "Competitor Y just raised $50M. How does that change your position?"
- "If they copy your best feature in 90 days, what's your moat?"
### Phase 3: Build the Narrative
The board meeting isn't a status update. It's a leadership demonstration.
**The structure that works:**
1. **Where we are (honest)** — Current state of business, the real number, not the smoothed one
2. **What we learned** — What the data is telling us that we didn't know 90 days ago
3. **What we got wrong** — Name it directly. Don't make them ask.
4. **What we're doing about it** — Specific, dated, owned actions
5. **What we need from this room** — Concrete ask. Not "support" — specific introductions, decisions, resources.
**The rule on bad news:** Never let the board be surprised. If a quarter went badly, they should know before the deck. A 5-sentence email 3 days before: "Revenue came in at $X vs $Y target. Here's what happened, here's what I'm doing, here's what I need from you."
### Phase 4: Adversarial Preparation
Do a mock board meeting. Have someone play the hardest director you have.
**The simulation:**
- Present your deck as you would
- The mock director asks every uncomfortable question
- You answer without referring to the deck
- After: note every question that made you pause or feel defensive
**The questions that made you defensive = the questions you need to prepare for.**
### Phase 5: Director-by-Director Prep
Not all board members want the same thing from a meeting.
**For each director, know:**
- Their primary concern right now (usually tied to their investment thesis)
- The metric they watch most closely
- What would make them lose confidence in you
- What they've said in the last meeting that you should address
**Common director types:**
- **The operator** — wants to know what's breaking and who owns fixing it
- **The financial investor** — focused on path to profitability or next raise
- **The strategic investor** — worried about competitive position and moat
- **The independent** — watching governance, team dynamics, and your judgment
---
## Pre-Meeting Checklist
**48 hours before:**
- [ ] All numbers verified against source systems (not last week's export)
- [ ] Deck reviewed for internal consistency
- [ ] Pre-read sent to board (deck + 1-page brief on key topics)
- [ ] One-on-ones done with any director likely to have concerns
- [ ] 3 hardest questions you expect — rehearsed out loud
**Day of meeting:**
- [ ] Agenda with time allocations distributed
- [ ] Know the ask for each agenda item (decision needed, input wanted, FYI)
- [ ] Materials to leave behind prepared
- [ ] Follow-up action template ready
---
## During the Meeting
**What the board is watching:**
- Do you own the bad news or deflect it?
- Are you defending a narrative or sharing reality?
- Do you know your numbers or do you look things up?
- When challenged, do you get defensive or engage?
- Do you know what you don't know?
**The single best thing you can do:** Name the hard thing before they do. "I want to address the revenue miss directly. Here's what happened, here's what I should have caught earlier, here's what changes."
---
## After the Meeting
Within 24 hours:
- Send action items with owners and dates
- Send any data you promised but didn't have
- Note the questions that came up you weren't ready for
- Schedule follow-up with any director who seemed unsatisfied
The next board prep starts now.
FILE:skills/challenge/SKILL.md
---
name: "challenge"
description: "/em -challenge — Pre-Mortem Plan Analysis"
---
# /em:challenge — Pre-Mortem Plan Analysis
**Command:** `/em:challenge <plan>`
Systematically finds weaknesses in any plan before reality does. Not to kill the plan — to make it survive contact with reality.
---
## The Core Idea
Most plans fail for predictable reasons. Not bad luck — bad assumptions. Overestimated demand. Underestimated complexity. Dependencies nobody questioned. Timing that made sense in a spreadsheet but not in the real world.
The pre-mortem technique: **imagine it's 12 months from now and this plan failed spectacularly. Now work backwards. Why?**
That's not pessimism. It's how you build something that doesn't collapse.
---
## When to Run a Challenge
- Before committing significant resources to a plan
- Before presenting to the board or investors
- When you notice you're only hearing positive feedback about the plan
- When the plan requires multiple external dependencies to align
- When there's pressure to move fast and "figure it out later"
- When you feel excited about the plan (excitement is a signal to scrutinize harder)
---
## The Challenge Framework
### Step 1: Extract Core Assumptions
Before you can test a plan, you need to surface everything it assumes to be true.
For each section of the plan, ask:
- What has to be true for this to work?
- What are we assuming about customer behavior?
- What are we assuming about competitor response?
- What are we assuming about our own execution capability?
- What external factors does this depend on?
**Common assumption categories:**
- **Market assumptions** — size, growth rate, customer willingness to pay, buying cycle
- **Execution assumptions** — team capacity, velocity, no major hires needed
- **Customer assumptions** — they have the problem, they know they have it, they'll pay to solve it
- **Competitive assumptions** — incumbents won't respond, no new entrant, moat holds
- **Financial assumptions** — burn rate, revenue timing, CAC, LTV ratios
- **Dependency assumptions** — partner will deliver, API won't change, regulations won't shift
### Step 2: Rate Each Assumption
For every assumption extracted, rate it on two dimensions:
**Confidence level (how sure are you this is true):**
- **High** — verified with data, customer conversations, market research
- **Medium** — directionally right but not validated
- **Low** — plausible but untested
- **Unknown** — we simply don't know
**Impact if wrong (what happens if this assumption fails):**
- **Critical** — plan fails entirely
- **High** — major delay or cost overrun
- **Medium** — significant rework required
- **Low** — manageable adjustment
### Step 3: Map Vulnerabilities
The matrix of Low/Unknown confidence × Critical/High impact = your highest-risk assumptions.
**Vulnerability = Low confidence + High impact**
These are not problems to ignore. They're the bets you're making. The question is: are you making them consciously?
### Step 4: Find the Dependency Chain
Many plans fail not because any single assumption is wrong, but because multiple assumptions have to be right simultaneously.
Map the chain:
- Does assumption B depend on assumption A being true first?
- If the first thing goes wrong, how many downstream things break?
- What's the critical path? What has zero slack?
### Step 5: Test the Reversibility
For each critical vulnerability: if this assumption turns out to be wrong at month 3, what do you do?
- Can you pivot?
- Can you cut scope?
- Is money already spent?
- Are commitments already made?
The less reversible, the more rigorously you need to validate before committing.
---
## Output Format
**Challenge Report: [Plan Name]**
```
CORE ASSUMPTIONS (extracted)
1. [Assumption] — Confidence: [H/M/L/?] — Impact if wrong: [Critical/High/Medium/Low]
2. ...
VULNERABILITY MAP
Critical risks (act before proceeding):
• [#N] [Assumption] — WHY it might be wrong — WHAT breaks if it is
High risks (validate before scaling):
• ...
DEPENDENCY CHAIN
[Assumption A] → depends on → [Assumption B] → which enables → [Assumption C]
Weakest link: [X] — if this breaks, [Y] and [Z] also fail
REVERSIBILITY ASSESSMENT
• Reversible bets: [list]
• Irreversible commitments: [list — treat with extreme care]
KILL SWITCHES
What would have to be true at [30/60/90 days] to continue vs. kill/pivot?
• Continue if: ...
• Kill/pivot if: ...
HARDENING ACTIONS
1. [Specific validation to do before proceeding]
2. [Alternative approach to consider]
3. [Contingency to build into the plan]
```
---
## Challenge Patterns by Plan Type
### Product Roadmap
- Are we building what customers will pay for, or what they said they wanted?
- Does the velocity estimate account for real team capacity (not theoretical)?
- What happens if the anchor feature takes 3× longer than estimated?
- Who owns decisions when requirements conflict?
### Go-to-Market Plan
- What's the actual ICP conversion rate, not the hoped-for one?
- How many touches to close, and do you have the sales capacity for that?
- What happens if the first 10 deals take 3 months instead of 1?
- Is "land and expand" a real motion or a hope?
### Hiring Plan
- What happens if the key hire takes 4 months to find, not 6 weeks?
- Is the plan dependent on retaining specific people who might leave?
- Does the plan account for ramp time (usually 3–6 months before full productivity)?
- What's the burn impact if headcount leads revenue by 6 months?
### Fundraising Plan
- What's your fallback if the lead investor passes?
- Have you modeled the timeline if it takes 6 months, not 3?
- What's your runway at current burn if the round closes at the low end?
- What assumptions break if you raise 50% of the target amount?
---
## The Hardest Questions
These are the ones people skip:
- "What's the bear case, not the base case?"
- "If this exact plan was run by a team we don't trust, would it work?"
- "What are we not saying out loud because it's uncomfortable?"
- "Who has incentives to make this plan sound better than it is?"
- "What would an enemy of this plan attack first?"
---
## Deliverable
The output of `/em:challenge` is not permission to stop. It's a vulnerability map. Now you can make conscious decisions: validate the risky assumptions, hedge the critical ones, or accept the bets you're making knowingly.
Unknown risks are dangerous. Known risks are manageable.
FILE:skills/hard-call/SKILL.md
---
name: "hard-call"
description: "/em -hard-call — Framework for Decisions With No Good Options"
---
# /em:hard-call — Framework for Decisions With No Good Options
**Command:** `/em:hard-call <decision>`
For the decisions that keep you up at 3am. Firing a co-founder. Laying off 20% of the team. Killing a product that customers love. Pivoting. Shutting down.
These decisions don't have a right answer. They have a less wrong answer. This framework helps you find it.
---
## Why These Decisions Are Hard
Not because the data is unclear. Often, the data is clear. They're hard because:
1. **Real people are affected** — someone loses a job, a relationship ends, a team is hurt
2. **You've been avoiding the decision** — which means the problem is already worse than it was
3. **Irreversibility** — unlike most business decisions, you can't undo this easily
4. **You have skin in the game** — your judgment about the right call is clouded by your feelings about it
The longer you avoid a hard call, the worse the situation usually gets. The company that needed a 10% cut 6 months ago now needs a 25% cut. The co-founder conversation that should have happened at month 4 is happening at month 14.
**Most hard decisions are late decisions.**
---
## The Framework
### Step 1: The Reversibility Test
The most important question first: **can you undo this?**
- **Reversible** — try it, learn, adjust (fire the vendor, kill the feature, change the strategy)
- **Partially reversible** — painful to undo but possible (restructure, change co-founder roles)
- **Irreversible** — cannot be undone (layoff a person, shut down a product with customer lock-in, close a legal entity)
For irreversible decisions, the bar for certainty is higher. You must do more due diligence before acting. Not because you might be wrong — but because you can't take it back.
**If you're treating a reversible decision like it's irreversible, you're avoiding it.**
### Step 2: The 10/10/10 Framework
Ask three questions about each option:
- **10 minutes from now**: How will you feel immediately after making this decision?
- **10 months from now**: What will the impact be? Will the problem be solved?
- **10 years from now**: When you look back, will this have been the right call?
The 10-minute feeling is usually the least reliable guide. The 10-year view usually clarifies what the right call actually is.
**Most hard decisions look obvious at 10 years. The question is whether you can tolerate the 10-minute pain.**
### Step 3: The Andy Grove Test
Andy Grove's test for strategic decisions: "If we got replaced tomorrow and a new CEO came in, what would they do?"
A fresh set of eyes, no emotional investment in the current path, no sunk cost. What's the obvious right call from the outside?
If the answer is clear to an outsider, the question becomes: why haven't you done it yet?
### Step 4: Stakeholder Impact Mapping
For each option, map who's affected and how:
| Stakeholder | Option A Impact | Option B Impact | Their reaction |
|-------------|----------------|----------------|----------------|
| Affected employees | | | |
| Remaining team | | | |
| Customers | | | |
| Investors | | | |
| You | | | |
This isn't about finding the option that hurts nobody — there isn't one. It's about understanding the full picture before you decide.
### Step 5: The Pre-Announcement Test
Before making the decision: write the announcement. The email to the team, the message to the customer, the conversation you'll have.
**If you can't write that announcement, you're not ready to make the decision.**
Writing it forces you to confront the reality of what you're doing. It also surfaces whether your reasoning holds under examination. "We're making this change because…" — does that sentence ring true?
### Step 6: The Communication Plan
Hard decisions almost always get harder if communication is bad. The decision itself is not the only thing that matters — how it's done matters enormously.
For every hard call, plan:
- **Who needs to know first** (the person directly affected, before anyone else)
- **How you'll tell them** (in person when possible, never via email for personal impact)
- **What you'll say** (honest, direct, compassionate — see `references/hard_things.md`)
- **What they can ask** (be ready for every question)
- **What comes next** (give them a clear picture of what happens after)
---
## Decision-Specific Frameworks
### Firing a Co-Founder
See `references/hard_things.md — Co-Founder Conflicts` for full framework.
Key questions to answer first:
- Is this a performance problem or a values/culture problem? (Different conversations)
- Have you been explicit — not hinted, but direct — about the problem?
- What does the cap table look like and what are the legal implications?
- Is there a role that works better for them, or is this a full exit?
- Who needs to know (board, team, investors) and in what order?
**The rule:** If you've been thinking about this for more than 3 months, you already know the answer. The question is when, not whether.
### Layoffs
Key questions:
- Is this a one-time reset or the beginning of a longer decline? (One reset is recoverable. Serial layoffs kill culture.)
- Are you cutting deep enough? (Insufficient layoffs are worse than no layoffs — two rounds destroys trust.)
- Who owns the announcement and is it direct and honest?
- What's the severance and is it fair?
- How do you prevent the best people from leaving after?
**The rule:** Cut once, cut deep, cut with dignity. Uncertainty is worse than clarity.
### Pivoting
Key questions:
- Is this a true pivot (new direction) or an optimization (same direction, different tactic)?
- What are you keeping and what are you abandoning?
- Do you have evidence the new direction works, or are you running from failure?
- How do you tell current customers who bought the old vision?
- What does this do to the board's confidence?
**The rule:** Pivots should be pulled by evidence of new opportunity, not pushed by failure of the current path.
### Killing a Product Line
Key questions:
- What happens to customers currently using it?
- What's the migration path?
- What do the people who built it do?
- Is "kill it" the right call or is "sell it" or "spin it out" better?
- What's the narrative — internally and externally?
---
## The Avoiding-It Test
You know you've been avoiding a hard call if:
- You've thought about it every week for more than a month
- You're hoping the situation will "resolve itself"
- You're waiting for more data that you'll never feel is enough
- You've had the conversation in your head many times but not in real life
- Other people around you have noticed the problem
**The cost of delay is almost always higher than the cost of the decision.**
Every month you wait, the problem compounds. The co-founder who's not working out becomes more entrenched. The product line that needs to die consumes more resources. The person who needs to be let go affects the people around them.
Make the call. Make it clearly. Make it with dignity.
FILE:skills/postmortem/SKILL.md
---
name: "postmortem"
description: "/em -postmortem — Honest Analysis of What Went Wrong"
---
# /em:postmortem — Honest Analysis of What Went Wrong
**Command:** `/em:postmortem <event>`
Not blame. Understanding. The failed deal, the missed quarter, the feature that flopped, the hire that didn't work out. What actually happened, why, and what changes as a result.
---
## Why Most Post-Mortems Fail
They become one of two things:
**The blame session** — someone gets scapegoated, defensive walls go up, actual causes don't get examined, and the same problem happens again in a different form.
**The whitewash** — "We learned a lot, we're going to do better, here are 12 vague action items." Nothing changes. Same problem, different quarter.
A real post-mortem is neither. It's a rigorous investigation into a system failure. Not "whose fault was it" but "what conditions made this outcome predictable in hindsight?"
**The purpose:** extract the maximum learning value from a failure so you can prevent recurrence and improve the system.
---
## The Framework
### Step 1: Define the Event Precisely
Before analysis: describe exactly what happened.
- What was the expected outcome?
- What was the actual outcome?
- When was the gap first visible?
- What was the impact (financial, operational, reputational)?
Precision matters. "We missed Q3 revenue" is not precise enough. "We closed $420K in new ARR vs $680K target — a $260K miss driven primarily by three deals that slipped to Q4 and one deal that was lost to a competitor" is precise.
### Step 2: The 5 Whys — Done Properly
The goal: get from **what happened** (the symptom) to **why it happened** (the root cause).
Standard bad 5 Whys:
- Why did we miss revenue? Because deals slipped.
- Why did deals slip? Because the sales cycle was longer than expected.
- Why? Because the customer buying process is complex.
- Why? Because we're selling to enterprise.
- Why? That's just how enterprise sales works.
→ Conclusion: Nothing to do. It's just enterprise.
Real 5 Whys:
- Why did we miss revenue? Three deals slipped out of quarter.
- Why did those deals slip? None of them had identified a champion with budget authority.
- Why did we progress deals without a champion? Our qualification criteria didn't require it.
- Why didn't our qualification criteria require it? When we built the criteria 8 months ago, we were in SMB, not enterprise.
- Why haven't we updated qualification criteria as ICP shifted? No owner, no process for criteria review.
→ Root cause: Qualification criteria outdated, no owner, no review process.
→ Fix: Update criteria, assign owner, add quarterly review.
**The test for a good root cause:** Could you prevent recurrence with a specific, concrete change? If yes, you've found something real.
### Step 3: Distinguish Contributing Factors from Root Cause
Most events have multiple contributing factors. Not all are root causes.
**Contributing factor:** Made it worse, but isn't the core reason. If removed, the outcome might have been different — but the same class of problem would recur.
**Root cause:** The fundamental condition that made the outcome probable. Fix this, and this class of problem doesn't recur.
Example — failed hire:
- Contributing factors: rushed process, reference checks skipped, team under pressure to staff up
- Root cause: No defined competency framework, so interview process varied by who happened to conduct interviews
**The distinction matters.** If you address only contributing factors, you'll have a different-looking but structurally identical failure next time.
### Step 4: Identify the Warning Signs That Were Ignored
Every failure has precursors. In hindsight, they're obvious. The value of this step is making them obvious prospectively.
Ask:
- At what point was the negative outcome predictable?
- What signals were visible at that point?
- Who saw them? What happened when they raised them?
- Why weren't they acted on?
**Common patterns:**
- Signal was raised but dismissed by a senior person
- Signal wasn't raised because nobody felt safe saying it
- Signal was seen but no one had clear ownership to act on it
- Data was available but nobody was looking at it
- The team was too optimistic to take negative signals seriously
This step is particularly important for systemic issues — "we didn't feel safe raising the concern" is a much deeper root cause than "the deal qualification was off."
### Step 5: Distinguish What Was in Control vs. Out of Control
Some failures happen despite correct decisions. Some happen because of incorrect decisions. Knowing the difference prevents both overcorrection and undercorrection.
- **In control:** Process, criteria, team capability, resource allocation, decisions made
- **Out of control:** Market conditions, customer decisions, competitor actions, macro events
For things out of control: what can be done to be more resilient to similar events?
For things in control: what specifically needs to change?
**Warning:** "It was outside our control" is sometimes used to avoid accountability. Be rigorous.
### Step 6: Build the Change Register
Every post-mortem ends with a change register — specific commitments, owned and dated.
**Bad action items:**
- "We'll improve our qualification process"
- "Communication will be better"
- "We'll be more rigorous about forecasting"
**Good action items:**
- "Ravi owns rewriting qualification criteria by March 15 to include champion identification as hard requirement. New criteria reviewed in weekly sales standup starting March 22."
- "By March 10, Elena adds deal-slippage risk flag to CRM for any open opportunity >60 days without a product demo"
- "Maria runs a 30-min retrospective with enterprise sales team every 6 weeks starting April 1, reviews win/loss data"
**For each action:**
- What exactly is changing?
- Who owns it?
- By when?
- How will you verify it worked?
### Step 7: Verification Date
The most commonly skipped step. Post-mortems are useless if nobody checks whether the changes actually happened and actually worked.
Set a verification date: "We'll review whether qualification criteria have been updated and whether deal slippage rate has improved at the June board meeting."
Without this, post-mortems are theater.
---
## Post-Mortem Output Format
```
EVENT: [Name and date]
EXPECTED: [What was supposed to happen]
ACTUAL: [What happened]
IMPACT: [Quantified]
TIMELINE
[Date]: [What happened or was visible]
[Date]: ...
5 WHYS
1. [Why did X happen?] → Because [Y]
2. [Why did Y happen?] → Because [Z]
3. [Why did Z happen?] → Because [A]
4. [Why did A happen?] → Because [B]
5. [Why did B happen?] → Because [ROOT CAUSE]
ROOT CAUSE: [One clear sentence]
CONTRIBUTING FACTORS
• [Factor] — how it contributed
• [Factor] — how it contributed
WARNING SIGNS MISSED
• [Signal visible at what date] — why it wasn't acted on
WHAT WAS IN CONTROL: [List]
WHAT WASN'T: [List]
CHANGE REGISTER
| Action | Owner | Due Date | Verification |
|--------|-------|----------|-------------|
| [Specific change] | [Name] | [Date] | [How to verify] |
VERIFICATION DATE: [Date of check-in]
```
---
## The Tone of Good Post-Mortems
Blame is cheap. Understanding is hard.
The goal isn't to establish that someone made a mistake. The goal is to understand why the system produced that outcome — so the system can be improved.
"The salesperson didn't qualify the deal properly" is blame.
"Our qualification framework hadn't been updated when we moved upmarket, and no one owned keeping it current" is understanding.
The first version fires or shames someone. The second version builds a more resilient organization.
Both might be true simultaneously. The distinction is: which one actually prevents recurrence?
FILE:skills/stress-test/SKILL.md
---
name: "stress-test"
description: "/em -stress-test — Business Assumption Stress Testing"
---
# /em:stress-test — Business Assumption Stress Testing
**Command:** `/em:stress-test <assumption>`
Take any business assumption and break it before the market does. Revenue projections. Market size. Competitive moat. Hiring velocity. Customer retention.
---
## Why Most Assumptions Are Wrong
Founders are optimists by nature. That's a feature — you need optimism to start something from nothing. But it becomes a liability when assumptions in business models get inflated by the same optimism that got you started.
**The most dangerous assumptions are the ones everyone agrees on.**
When the whole team believes the $50M market is real, when every investor call goes well so you assume the round will close, when your model shows $2M ARR by December and nobody questions it — that's when you're most exposed.
Stress testing isn't pessimism. It's calibration.
---
## The Stress-Test Methodology
### Step 1: Isolate the Assumption
State it explicitly. Not "our market is large" but "the total addressable market for B2B spend management software in German SMEs is €2.3B."
The more specific the assumption, the more testable it is. Vague assumptions are unfalsifiable — and therefore useless.
**Common assumption types:**
- **Market size** — TAM, SAM, SOM; growth rate; customer segments
- **Customer behavior** — willingness to pay, churn, expansion, referrals
- **Revenue model** — conversion rates, deal size, sales cycle, CAC
- **Competitive position** — moat durability, competitor response speed, switching cost
- **Execution** — team velocity, hire timeline, product timeline, operational scaling
- **Macro** — regulatory environment, economic conditions, technology availability
### Step 2: Find the Counter-Evidence
For every assumption, actively search for evidence that it's wrong.
Ask:
- Who has tried this and failed?
- What data contradicts this assumption?
- What does the bear case look like?
- If a smart skeptic was looking at this, what would they point to?
- What's the base rate for assumptions like this?
**Sources of counter-evidence:**
- Comparable companies that failed in adjacent markets
- Customer churn data from similar businesses
- Historical accuracy of similar forecasts
- Industry reports with conflicting data
- What competitors who tried this found
The goal isn't to find a reason to stop — it's to surface what you don't know.
### Step 3: Model the Downside
Most plans model the base case and the upside. Stress testing means modeling the downside explicitly.
**For quantitative assumptions (revenue, growth, conversion):**
| Scenario | Assumption Value | Probability | Impact |
|----------|-----------------|-------------|--------|
| Base case | [Original value] | ? | |
| Bear case | -30% | ? | |
| Stress case | -50% | ? | |
| Catastrophic | -80% | ? | |
Key question at each level: **Does the business survive? Does the plan make sense?**
**For qualitative assumptions (moat, product-market fit, team capability):**
- What's the earliest signal this assumption is wrong?
- How long would it take you to notice?
- What happens between when it breaks and when you detect it?
### Step 4: Calculate Sensitivity
Some assumptions matter more than others. Sensitivity analysis answers: **if this one assumption changes, how much does the outcome change?**
Example:
- If CAC doubles, how does that change runway?
- If churn goes from 5% to 10%, how does that change NRR in 24 months?
- If the deal cycle is 6 months instead of 3, how does that affect Q3 revenue?
High sensitivity = the assumption is a key lever. Wrong = big problem.
### Step 5: Propose the Hedge
For every high-risk assumption, there should be a hedge:
- **Validation hedge** — test it before betting on it (pilot, customer conversation, small experiment)
- **Contingency hedge** — if it's wrong, what's plan B?
- **Early warning hedge** — what's the leading indicator that would tell you it's breaking before it's too late to act?
---
## Stress Test Patterns by Assumption Type
### Revenue Projections
**Common failures:**
- Bottom-up model assumes 100% of pipeline converts
- Doesn't account for deal slippage, churn, seasonality
- New channel assumed to work before tested at scale
**Stress questions:**
- What's your actual historical win rate on pipeline?
- If your top 3 deals slip to next quarter, what happens to the number?
- What's the model look like if your new sales rep takes 4 months to ramp, not 2?
- If expansion revenue doesn't materialize, what's the growth rate?
**Test:** Build the revenue model from historical win rates, not hoped-for ones.
### Market Size
**Common failures:**
- TAM calculated top-down from industry reports without bottoms-up validation
- Conflating total market with serviceable market
- Assuming 100% of SAM is reachable
**Stress questions:**
- How many companies in your ICP actually exist and can you name them?
- What's your serviceable obtainable market in year 1-3?
- What percentage of your ICP is currently spending on any solution to this problem?
- What does "winning" look like and what market share does that require?
**Test:** Build a list of target accounts. Count them. Multiply by ACV. That's your SAM.
### Competitive Moat
**Common failures:**
- Moat is technology advantage that can be built in 6 months
- Network effects that haven't yet materialized
- Data advantage that requires scale you don't have
**Stress questions:**
- If a well-funded competitor copied your best feature in 90 days, what do customers do?
- What's your retention rate among customers who have tried alternatives?
- Is the moat real today or theoretical at scale?
- What would it cost a competitor to reach feature parity?
**Test:** Ask churned customers why they left and whether a competitor could have kept them.
### Hiring Plan
**Common failures:**
- Time-to-hire assumes standard recruiting cycle, not current market
- Ramp time not modeled (3-6 months before full productivity)
- Key hire dependency: plan only works if specific person is hired
**Stress questions:**
- What happens if the VP Sales hire takes 5 months, not 2?
- What does execution look like if you only hire 70% of planned headcount?
- Which single person, if they left tomorrow, would most damage the plan?
- Is the plan achievable with current team if hiring freezes?
**Test:** Model the plan with 0 net new hires. What still works?
### Competitive Response
**Common failures:**
- Assumes incumbents won't respond (they will if you're winning)
- Underestimates speed of response
- Doesn't model resource asymmetry
**Stress questions:**
- If the market leader copies your product in 6 months, how does pricing change?
- What's your response if a competitor raises $30M to attack your space?
- Which of your customers have vendor relationships with your competitors?
---
## The Stress Test Output
```
ASSUMPTION: [Exact statement]
SOURCE: [Where this came from — model, investor pitch, team gut feel]
COUNTER-EVIDENCE
• [Specific evidence that challenges this assumption]
• [Comparable failure case]
• [Data point that contradicts the assumption]
DOWNSIDE MODEL
• Bear case (-30%): [Impact on plan]
• Stress case (-50%): [Impact on plan]
• Catastrophic (-80%): [Impact on plan — does the business survive?]
SENSITIVITY
This assumption has [HIGH / MEDIUM / LOW] sensitivity.
A 10% change → [X] change in outcome.
HEDGE
• Validation: [How to test this before betting on it]
• Contingency: [Plan B if it's wrong]
• Early warning: [Leading indicator to watch — and at what threshold to act]
```
People leadership for scaling companies. Hiring strategy, compensation design, org structure, culture, and retention. Use when building hiring plans, designi...
---
name: "chro-advisor"
description: "People leadership for scaling companies. Hiring strategy, compensation design, org structure, culture, and retention. Use when building hiring plans, designing comp frameworks, restructuring teams, managing performance, building culture, or when user mentions CHRO, HR, people strategy, talent, headcount, compensation, org design, retention, or performance management."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: chro-leadership
updated: 2026-03-05
python-tools: hiring_plan_modeler.py, comp_benchmarker.py
frameworks: people-strategy, comp-frameworks, org-design
---
# CHRO Advisor
People strategy and operational HR frameworks for business-aligned hiring, compensation, org design, and culture that scales.
## Keywords
CHRO, chief people officer, CPO, HR, human resources, people strategy, hiring plan, headcount planning, talent acquisition, recruiting, compensation, salary bands, equity, org design, organizational design, career ladder, title framework, retention, performance management, culture, engagement, remote work, hybrid, spans of control, succession planning, attrition
## Quick Start
```bash
python scripts/hiring_plan_modeler.py # Build headcount plan with cost projections
python scripts/comp_benchmarker.py # Benchmark salaries and model total comp
```
## Core Responsibilities
### 1. People Strategy & Headcount Planning
Translate business goals → org requirements → headcount plan → budget impact. Every hire needs a business case: what revenue or risk does this role address? See `references/people_strategy.md` for hiring at each growth stage.
### 2. Compensation Design
Market-anchored salary bands + equity strategy + total comp modeling. See `references/comp_frameworks.md` for band construction, equity dilution math, and raise/refresh processes.
### 3. Org Design
Right structure for the stage. Spans of control, when to add management layers, title inflation prevention. See `references/org_design.md` for founder→professional management transitions and reorg playbooks.
### 4. Retention & Performance
Retention starts at hire. Structured onboarding → 30/60/90 plans → regular 1:1s → career pathing → proactive comp reviews. See `references/people_strategy.md` for what actually moves the needle.
**Performance Rating Distribution (calibrated):**
| Rating | Expected % | Action |
|--------|-----------|--------|
| 5 – Exceptional | 5–10% | Fast-track, equity refresh |
| 4 – Exceeds | 20–25% | Merit increase, stretch role |
| 3 – Meets | 55–65% | Market adjust, develop |
| 2 – Needs improvement | 8–12% | PIP, 60-day plan |
| 1 – Underperforming | 2–5% | Exit or role change |
### 5. Culture & Engagement
Culture is behavior, not values on a wall. Measure eNPS quarterly. Act on results within 30 days or don't ask.
## Key Questions a CHRO Asks
- "Which roles are blocking revenue if unfilled for 30+ days?"
- "What's our regrettable attrition rate? Who left that we wish hadn't?"
- "Are managers our retention asset or our attrition cause?"
- "Can a new hire explain their career path in 12 months?"
- "Where are we paying below P50? Who's a flight risk because of it?"
- "What's the cost of this hire vs. the cost of not hiring?"
## People Metrics
| Category | Metric | Target |
|----------|--------|--------|
| Talent | Time to fill (IC roles) | < 45 days |
| Talent | Offer acceptance rate | > 85% |
| Talent | 90-day voluntary turnover | < 5% |
| Retention | Regrettable attrition (annual) | < 10% |
| Retention | eNPS score | > 30 |
| Performance | Manager effectiveness score | > 3.8/5 |
| Comp | % employees within band | > 90% |
| Comp | Compa-ratio (avg) | 0.95–1.05 |
| Org | Span of control (ICs) | 6–10 |
| Org | Span of control (managers) | 4–7 |
## Red Flags
- Attrition spikes and exit interviews all name the same manager
- Comp bands haven't been refreshed in 18+ months
- No career ladder → top performers leave after 18 months
- Hiring without a written business case or job scorecard
- Performance reviews happen once a year with no mid-year check-in
- Equity refreshes only for executives, not high performers
- Time to fill > 90 days for critical roles
- eNPS below 0 — something is structurally broken
- More than 3 org layers between IC and CEO at < 50 people
## Integration with Other C-Suite Roles
| When... | CHRO works with... | To... |
|---------|-------------------|-------|
| Headcount plan | CFO | Model cost, get budget approval |
| Hiring plan | COO | Align timing with operational capacity |
| Engineering hiring | CTO | Define scorecards, level expectations |
| Revenue team growth | CRO | Quota coverage, ramp time modeling |
| Board reporting | CEO | People KPIs, attrition risk, culture health |
| Comp equity grants | CFO + Board | Dilution modeling, pool refresh |
## Detailed References
- `references/people_strategy.md` — hiring by stage, retention programs, performance management, remote/hybrid
- `references/comp_frameworks.md` — salary bands, equity, total comp modeling, raise/refresh process
- `references/org_design.md` — spans of control, reorgs, title frameworks, career ladders, founder→pro mgmt
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- Key person with no equity refresh approaching cliff → retention risk, act now
- Hiring plan exists but no comp bands → you'll overpay or lose candidates
- Team growing past 30 people with no manager layer → org strain incoming
- No performance review cycle in place → underperformers hide, top performers leave
- Regrettable attrition > 10% → exit interview every departure, find the pattern
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Build a hiring plan" | Headcount plan with roles, timing, cost, and ramp model |
| "Set up comp bands" | Compensation framework with bands, equity, benchmarks |
| "Design our org" | Org chart proposal with spans, layers, and transition plan |
| "We're losing people" | Retention analysis with risk scores and intervention plan |
| "People board section" | Headcount, attrition, hiring velocity, engagement, risks |
## Reasoning Technique: Empathy + Data
Start with the human impact, then validate with metrics. Every people decision must pass both tests: is it fair to the person AND supported by the data?
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:references/comp_frameworks.md
# Compensation Frameworks Reference
Salary bands, equity design, total comp modeling, comp philosophy, and raise/refresh processes.
---
## Comp Philosophy — The Foundation
Before building bands, define your philosophy. Ambiguity in comp philosophy = pay equity lawsuits and trust erosion.
**The five decisions:**
### 1. What market percentile do you target?
- **P25 (below market):** Only viable with exceptional mission, equity, or growth opportunity. Flight risk is high after 18 months.
- **P50 (market median):** Standard for most Series A–B companies. Competitive without premium.
- **P75 (above market):** Premium talent strategy. Used by high-margin or talent-intensive businesses. Netflix model.
- **P90+:** Top-of-market for specific functions (ML at AI companies, senior engineers at FAANG feeders).
**Common hybrid:** P50 base + above-market equity = total comp at P65–75.
### 2. What's in your total comp package?
Define each component explicitly:
- **Base salary** — cash, market-benchmarked
- **Variable / bonus** — % of base, tied to what criteria
- **Equity** — options vs. RSUs, vesting schedule, refresh cadence
- **Benefits** — health, retirement, PTO policy
- **Learning & development budget**
- **Remote/location allowances**
### 3. Are bands public internally?
Recommended: Yes. Pay transparency reduces equity complaints, builds trust, and forces you to maintain clean bands.
### 4. How often do you refresh bands?
Minimum: annually. High-growth markets: every 6 months (engineering specifically in hot markets).
### 5. How do you handle individual negotiation?
Options:
- **Fixed bands, no negotiation** (Buffer model) — simple, fair, loses some candidates
- **Band range with manager discretion** — most common, requires calibration guardrails
- **Individual negotiation within band** — flexible, creates pay equity drift over time
---
## Salary Bands: Construction
### Step 1: Define levels
Standard IC levels (adapt to company):
| Level | Title example | Scope |
|-------|--------------|-------|
| L1 | Junior / Associate | Execution with guidance |
| L2 | Mid-level | Independent execution |
| L3 | Senior | Leads workstreams, mentors L1-L2 |
| L4 | Staff / Principal | Cross-team technical leadership |
| L5 | Distinguished / Fellow | Company-wide technical direction |
Management track:
| Level | Title | Scope |
|-------|-------|-------|
| M1 | Manager | Team of 4–8 ICs |
| M2 | Senior Manager | Manager of managers or larger team |
| M3 | Director | Function or large org |
| M4 | VP | Business unit, company-wide |
| M5 | SVP / C-Suite | Executive |
### Step 2: Gather market data
**Data sources (by quality):**
1. **Radford / Aon** — Gold standard. Expensive ($10K+/year). Worth it at Series B+.
2. **Levels.fyi** — Excellent for engineering. Free. Self-reported but large sample.
3. **Glassdoor Salary** — Broad coverage. Less precise for startups.
4. **Pave / Carta Total Comp** — VC-backed companies. Good peer benchmarking.
5. **LinkedIn Salary** — Free tier. Reasonable signal for G&A roles.
6. **Offer letter data** — What candidates are bringing from other companies. Real-time signal.
**What to pull:** P25, P50, P75, P90 for each role × level × geography.
### Step 3: Set band structure
**Band width (range within a level):**
- IC bands: 80–120% of midpoint (i.e., ±20% from center)
- Manager bands: 85–115% of midpoint
- Wider bands allow room for differentiation within level; narrower bands reduce pay equity drift
**Band overlap between levels:**
- 10–20% overlap is normal (top of L2 overlaps with bottom of L3)
- > 30% overlap: your levels are too close together
- No overlap: new hires jump too much between levels (compression risk)
**Example engineering band structure (US, Series B company, P50 target):**
| Level | Band Min | Midpoint | Band Max |
|-------|----------|----------|----------|
| L1 Software Engineer | $90K | $105K | $125K |
| L2 Software Engineer | $115K | $135K | $160K |
| L3 Senior SWE | $150K | $175K | $205K |
| L4 Staff SWE | $195K | $225K $260K |
| M1 Eng Manager | $175K | $205K | $235K |
| M2 Sr Eng Manager | $215K | $250K | $285K |
| M3 Director, Eng | $255K | $300K | $345K |
*Adjust by 15–25% for non-SF/NYC markets. Adjust -40% to -60% for European markets.*
### Step 4: Place employees in bands
**Compa-ratio** = Employee salary / Band midpoint
| Compa-ratio | Interpretation |
|------------|---------------|
| < 0.85 | Below range — immediate risk |
| 0.85–0.95 | Developing in role |
| 0.95–1.05 | Fully performing (target zone) |
| 1.05–1.15 | Senior/expert in role |
| > 1.15 | Above range — flag for review |
**Audit report:** Run quarterly. Flag anyone below 0.85 (flight risk) or above 1.15 (overpaid for level, or needs promotion).
---
## Equity Frameworks for Startups
### Option Basics
**ISO vs NSO:**
- ISO (Incentive Stock Options): For employees. Favorable tax treatment if held 1+ year post-exercise.
- NSO (Non-Qualified Stock Options): For advisors, contractors, sometimes employees. Taxed as ordinary income on exercise.
**Strike price:** Set to 409A valuation at grant. Lower is better for employees. Early employees win on strike price.
**Vesting schedule standards:**
- 4-year vest, 1-year cliff: Standard
- 4-year vest, 6-month cliff: Startup market adapting to faster pace
- 1-year cliff means: nothing until 12 months; monthly or quarterly after
**Post-termination exercise window (PTEW):**
- Standard: 90 days. Often too short for employees who can't afford exercise.
- Better: 1–5 years or until IPO. Use as a talent differentiator.
- Companies extending PTEW: Stripe, Airbnb (pre-IPO), Square, most employee-friendly startups.
### Equity Grant Ranges by Stage and Level
*Expressed as % of fully diluted shares at grant. Ranges vary significantly by market, stage, and funding.*
**Seed stage:**
| Role | Equity % |
|------|----------|
| Co-founder | 20–40% |
| First engineering hire | 0.5–1.5% |
| First non-technical exec hire | 0.25–0.75% |
| IC (L2-L3) | 0.1–0.4% |
| IC (L3-L4) | 0.2–0.6% |
**Series A:**
| Role | Equity % |
|------|----------|
| VP / Head of function | 0.3–0.75% |
| Director | 0.1–0.3% |
| Senior IC (L3) | 0.05–0.15% |
| Mid IC (L2) | 0.02–0.08% |
| Junior IC (L1) | 0.01–0.05% |
**Series B:**
| Role | Equity % |
|------|----------|
| VP / Head of function | 0.1–0.3% |
| Director | 0.05–0.15% |
| Senior IC (L3) | 0.02–0.07% |
| Mid IC (L2) | 0.01–0.03% |
*At Series B+, equity is increasingly expressed in dollar value (grant value = X shares × current 409A). Use Carta or Pulley to model dilution.*
### Equity Refresh Program
**Why it matters:** Employees hired at Series A with 4-year vesting will be fully vested by Series B. No unvested equity = no retention hook.
**When to refresh:**
- After every significant funding round
- Annually for high performers (top 20%)
- After promotion (role-commensurate top-up)
- Counter-offer situations (use carefully — signals you underpaid initially)
**Refresh models:**
1. **Anniversary grant:** Annual cliff-free refresh for all employees above a performance threshold
2. **Evergreen model:** Continuous vesting maintained — refresh annually so employee always has 2–3 years remaining
3. **Event-based:** Refresh tied to milestones (promotion, funding, annual review cycle)
**Dilution awareness:** Every refresh dilutes existing shareholders. Model pool usage quarterly. Replenish option pool before it drops below 10–12% of fully diluted shares.
---
## Total Comp Modeling
### Components of Total Comp
```
Total Compensation = Base Salary
+ Annual Bonus (target %)
+ Equity Value (annualized grant / vesting period)
+ Benefits (employer-paid premiums, retirement match)
+ Allowances (home office, internet, L&D, commuter)
```
### Annualizing Equity Value
For comparison to cash compensation:
```
Annual equity value = (Grant shares × Current 409A price) / Vesting years
```
Example: 10,000 options at $2 strike, current 409A = $8, 4-year vest
- Grant value at current 409A = 10,000 × $8 = $80,000
- Annual value = $80,000 / 4 = $20,000/year
- If base is $150K, total comp is ~$170K/year
*Note: For recruiting purposes, you can use last preferred share price (VC price) to show upside — but be transparent about the difference between 409A and preferred.*
### Benefits Valuation
Frequently undervalued in offers. Quantify explicitly:
| Benefit | Typical employer cost |
|---------|----------------------|
| Health insurance (employee) | $4K–8K/year |
| Health insurance (family) | $15K–25K/year |
| 401K match (4% of salary) | $5K–10K/year |
| L&D budget ($2K/year) | $2K/year |
| Home office stipend ($500) | $500/year |
A $140K offer with family health coverage + 4% 401K match is worth $165K+ total.
---
## Raise and Refresh Process
### Annual Compensation Review Cycle
**Recommended cadence:**
- October/November: Market data refresh, band updates
- November/December: Manager merit recommendations
- December/January: Calibration and approvals
- January/February: Effective date for new salaries + equity grants
**Budget allocation:**
- **Merit budget** (performance-based raises): 3–5% of total payroll typically
- **Market adjustment budget** (fixing below-band salaries): Separate from merit. Non-negotiable to avoid attrition.
- **Promotion budget:** Separate. Promotions should not come from merit pool.
### Merit Increase Guidelines
| Performance Rating | Merit Increase Range |
|-------------------|---------------------|
| 5 – Exceptional | 8–15% |
| 4 – Exceeds | 5–8% |
| 3 – Meets | 2–4% |
| 2 – Needs improvement | 0–1% |
| 1 – Underperforming | 0% (PIP active) |
*Adjust based on compa-ratio. A high performer at P90 of their band gets a smaller increase than a high performer at P50.*
### Compa-Ratio Adjustment Matrix
| Performance \ Compa-Ratio | < 0.90 | 0.90–1.00 | 1.00–1.10 | > 1.10 |
|---------------------------|--------|-----------|-----------|--------|
| Exceptional (5) | 12–15% | 8–12% | 5–8% | 3–5% |
| Exceeds (4) | 8–12% | 5–8% | 3–5% | 1–3% |
| Meets (3) | 5–8% | 3–5% | 2–3% | 0–2% |
| Needs impr (2) | 0–2% | 0–1% | 0% | 0% |
### Promotion vs. Merit — Keep These Separate
**Common mistake:** Using merit budget to fund promotions. This forces a choice between rewarding performance and recognizing level change.
**Promotion increase guidelines:**
- One level (e.g., L2 → L3): 10–20% increase, new equity grant
- Two levels (rare): 20–35% increase, new equity grant at new level
- Manager track (IC → M1): 15–25% increase, new equity grant
**Promotion criteria process:**
1. Manager nominates with written business case
2. Calibration committee reviews cross-functionally
3. HR validates against band (no off-band exceptions without CHRO sign-off)
4. Employee informed before annual review — never surprised at review meeting
### Off-Cycle Adjustments
When to do them:
- Counter-offer situations (see below)
- Competitive intelligence reveals underpay for a specific role
- New market data shows a role significantly under-benchmarked
- Internal equity audit reveals unexplained gaps
**Counter-offer policy:**
Three options:
1. **Match** — Risk: signals you underpay; sets precedent
2. **Partial match** — "We can do X, which is the top of your band" — cleaner
3. **Decline** — Accept the attrition, improve the band for the next hire
**Rule:** If you're regularly in counter-offer conversations, your bands are stale. Fix the bands.
---
## Pay Equity Audit
Run annually. Non-negotiable at Series B+.
**What to audit:**
- Pay gap by gender within each level and function
- Pay gap by ethnicity within each level and function
- Compa-ratio distribution across demographics
- Time-to-promotion by demographic group
**Methodology:**
1. Pull all employee data: level, function, salary, tenure, performance ratings, gender, ethnicity
2. Run regression controlling for level, tenure, and performance
3. Unexplained gap after controls = the problem to fix
4. Flag and remediate within the same review cycle
**Legal exposure:** In many jurisdictions, documented pay gaps without remediation plans are litigation risk. The audit creates a record of intent; remediation closes the risk.
**Remediation budget:** Set aside 0.5–1% of payroll annually for equity adjustments. If you're doing it right, this shrinks over time.
FILE:references/org_design.md
# Org Design Reference
Spans of control, layering decisions, reorgs, title frameworks, career ladders, and the founder→professional management transition.
---
## Core Org Design Principles
1. **Structure follows strategy.** Reorg after strategy shifts, not before.
2. **Optimize for the bottleneck.** Where does work get slow? Design around that.
3. **Minimize coordination cost.** Conway's Law: your org structure becomes your product architecture. Design intentionally.
4. **Bias toward flatness until it breaks.** Adding layers adds cost and slows decisions.
5. **Reorgs have transition costs.** Relationships reset. Count the cost before you restructure.
---
## Spans of Control
Span of control = number of direct reports a manager has.
### Benchmarks
| Role Type | Optimal Span | Min | Max |
|-----------|-------------|-----|-----|
| IC manager (predictable work) | 7–10 | 5 | 12 |
| IC manager (complex/creative work) | 5–7 | 4 | 8 |
| Manager of managers | 4–6 | 3 | 7 |
| VP / Director | 4–7 | 3 | 8 |
| C-Suite | 5–9 | 4 | 10 |
**Too narrow (< 4 ICs):** Over-management, high cost per output, manager becomes a bottleneck
**Too wide (> 12 ICs):** Under-management, degraded 1:1 quality, feedback loops collapse
### Factors that allow wider spans
- Highly autonomous, senior team (L3+ ICs)
- Predictable, well-defined work (support, ops)
- Strong tooling and process (reduces manager overhead)
- Experienced manager
### Factors that require narrower spans
- High-complexity, undefined problems (research, early product)
- Junior or newly promoted team members
- High interdependence between reports (coordination overhead)
- Manager is also an IC contributor (player-coach)
---
## When to Add Management Layers
**The wrong reason to add layers:** "We need to give good people somewhere to grow."
**The right reason:** "This manager has too many direct reports to do the job well."
### Layer triggers by growth stage
**0 → 15 people:** No layers. Everyone reports to founders.
**15 → 30 people:** First managers emerge. Usually technical leads or function leads. Should still be player-coaches.
**30 → 60 people:** Second layer forms. Engineering splits into squads. Sales gets a frontline manager. Each function has a head.
**60 → 150 people:** Director layer becomes necessary in large functions. Engineering VP + Engineering Directors + Team Managers.
**150+ people:** VP layer fully staffed. Senior Director / Director split. Clear IC → M → Senior M → Director → VP paths.
### The Rule of 7
When any manager has 7 or more direct reports and:
- 1:1s are skipped regularly
- Feedback quality drops
- Manager can't answer "how is each person doing?" without checking notes
→ Time to split or hire a manager.
### Management overhead cost
Every manager layer costs 10–15% in decision speed (communication hops).
Every management role without a team = pure overhead.
**Litmus test for each management role:**
- Does this person have at least 4 ICs under them?
- Would removing this role improve decision speed?
- Is this a management job or a "we ran out of IC levels" job?
---
## Functional vs. Product Org Structures
### Functional Structure (by discipline)
```
CEO
├── VP Engineering
│ ├── Backend Team
│ ├── Frontend Team
│ └── DevOps
├── VP Product
│ ├── PM (Feature A)
│ └── PM (Feature B)
└── VP Design
└── UX Designers
```
**Best for:** Early stage, < 100 people, single product
**Advantage:** Deep expertise development, clear career paths per discipline
**Disadvantage:** Cross-functional coordination is heavy; features require synchronization across silos
### Product/Pod Structure (by product area)
```
CEO
├── Product Area A (autonomous team)
│ ├── EM
│ ├── PM
│ └── Designer
├── Product Area B (autonomous team)
│ ├── EM
│ ├── PM
│ └── Designer
└── Platform (shared services)
└── Platform EM + team
```
**Best for:** Multiple products or large user segments, 50+ in product/eng
**Advantage:** Speed and autonomy; less cross-team coordination for most features
**Disadvantage:** Duplication risk; harder to maintain technical coherence; harder career paths
### When to shift from Functional → Product org
- You have 2+ distinct product lines that rarely share features
- Cross-functional feature delivery takes > 3 sprints of coordination overhead
- Teams are > 8 engineers and still waiting on shared resources
### Hybrid / Matrix (avoid unless necessary)
Matrix reporting (e.g., engineer reports to EM + PM) creates accountability confusion. Avoid at < 500 people.
---
## Title Frameworks
### The Problem with Title Inflation
Early startups over-title to compete with cash. "VP of Engineering" with 2 reports. "Head of Marketing" with no team.
**Consequences:**
- Can't add leadership above inflated titles without awkward conversations
- Candidates from mature companies expect scope commensurate with titles
- Internal equity breaks when the same title means different things
### Preventing Title Inflation
**Rule 1:** VP titles require managing managers (not just ICs).
**Rule 2:** Director titles require managing multiple ICs or a large function.
**Rule 3:** No more than one "Head of X" per function.
**Rule 4:** Document scope expectations per title before making offers.
### Engineering Title Ladder (example)
| Title | Level | Scope | Reports |
|-------|-------|-------|---------|
| Software Engineer I | L1 | Executes defined tasks | — |
| Software Engineer II | L2 | Independent delivery | — |
| Senior Software Engineer | L3 | Leads features, mentors | — |
| Staff Software Engineer | L4 | Cross-team technical leadership | — |
| Principal Software Engineer | L5 | Company-wide technical direction | — |
| Distinguished Engineer | L6 | External recognition, defining practice | — |
| Engineering Manager | M1 | Team of 4–8 engineers | 4–8 ICs |
| Senior Engineering Manager | M2 | Larger team or manager of managers | 2–4 managers |
| Director of Engineering | M3 | Functional area | Multiple managers |
| VP of Engineering | M4 | Engineering org | Directors |
| CTO | M5 | Technical organization + strategy | VPs |
**IC vs. Management track:** Explicitly separate. Senior ICs should not need to move to management for career advancement. Staff/Principal/Distinguished track provides this.
### Go-to-Market Title Ladder (example)
| Title | Level | Focus |
|-------|-------|-------|
| SDR / BDR | S1 | Outbound prospecting |
| Account Executive I | S2 | SMB closing |
| Account Executive II | S3 | Mid-market closing |
| Senior Account Executive | S4 | Enterprise closing |
| Principal / Strategic AE | S5 | Named accounts, complex deals |
| Sales Manager | M1 | 6–8 reps |
| Director of Sales | M2 | Multiple teams or segments |
| VP of Sales | M3 | Full sales org |
| CRO | M4 | Revenue org (sales + CS + marketing) |
---
## Career Ladders
A career ladder is a documented set of expectations per level. Not aspirational — behavioral. "What does a P3 engineer do that a P2 doesn't?"
### Why career ladders matter for HR
1. **Retention:** Employees can see where they're going
2. **Consistency:** Managers use the same criteria for promotions
3. **Compensation:** Bands anchor to levels; levels require definitions
4. **Equity:** Removes "who's the manager's favorite" from promotion decisions
### Career Ladder Structure
For each level, define 4 dimensions:
**1. Scope** — How big is the problem space? Team / cross-team / org-wide / company-wide?
**2. Impact** — How does work connect to outcomes? (Task → Feature → Product → Business)
**3. Craft** — Technical/functional skill expectations
**4. Influence** — How does this person improve others? (Self → peers → team → org)
**Example: Senior Software Engineer (L3) vs. Staff Software Engineer (L4)**
| Dimension | L3 (Senior SWE) | L4 (Staff SWE) |
|-----------|----------------|----------------|
| Scope | Owns features or services | Owns technical domains across teams |
| Impact | Ships features that improve user outcomes | Shapes technical direction for a product area |
| Craft | Writes high-quality code, good design skills | Sets coding standards, contributes to architecture |
| Influence | Mentors L1–L2, code reviews | Mentors L3+, identifies org-wide technical gaps |
### How to build a career ladder from scratch
1. **Interview your best performers** — "What do you do that your junior peers don't?" Collect behaviors, not aspirations.
2. **Draft 3 levels** — Don't start with 6. Start with junior, mid, senior. Add staff/principal only when you have enough people to warrant it.
3. **Manager calibration** — Every manager rates 5 current employees against the draft. Gaps surface immediately.
4. **Publish and iterate** — Don't wait for perfection. A 70% ladder shipped is better than a 100% ladder in a drawer.
---
## Reorg Playbook
### When reorgs are necessary
- Strategy pivot requires different team structure (e.g., single product → multi-product)
- Acquisition or team merger
- Function is genuinely too slow due to coordination overhead
- Leadership departure creates structural opportunity
### When reorgs are a mistake
- "We need to shake things up" (disruption for its own sake)
- Avoiding a specific personnel decision (use the right tool)
- Solving a cultural problem with a structural change
- Reacting to one team's complaint without systemic evidence
### Reorg Process (4–8 weeks)
**Week 1–2: Diagnose**
- Map current org: every role, reporting line, team output
- Identify where work is slow, duplicated, or falling through cracks
- Interview 5–10 people across teams: "What takes longer than it should? What decisions are hard to make?"
**Week 3–4: Design options**
- Draft 2–3 structural alternatives
- For each: estimated coordination costs, manager span impact, open roles created
- Validate with CEO + 1–2 trusted operators. Don't crowdsource the design.
**Week 5–6: Decide and prepare**
- Select option; finalize all reporting changes
- Prepare communications for every affected person (individual conversations before all-hands)
- Write the "why" — employees need to understand the business reason, not just the result
**Week 7–8: Communicate and implement**
- Individual conversations with all manager+ changes (first)
- Team-level conversations with managers (second)
- All-hands with full context (third)
- Updated org chart published within 24 hours of announcement
### Communication sequence (non-negotiable)
1. Affected individuals first (private, before anything else)
2. Affected managers second (to prepare for team conversations)
3. Full team/company third (all-hands or company note)
4. External (clients, board) only if materially impacted
**Never:** Email blast first. No individual conversations. Discovered on the org chart.
---
## Founder → Professional Management Transition
The most common scaling failure point in startups.
### Stage 1: Founder-Led (0–30 people)
Founders make all decisions, know everyone personally, set culture through behavior. Works because trust and context are built directly.
**What breaks:**
- Decisions bottleneck at founders
- New hires don't get enough context (founders can't be everywhere)
- Culture transmitted through osmosis, not documentation
### Stage 2: First Managers (30–80 people)
Founders can no longer manage all ICs. First manager layer typically = promoted high performers.
**The "brilliant IC → struggling manager" trap:**
- Individual contributor skills ≠ management skills
- Promoted ICs often continue doing IC work while ignoring management work
- No one holds them accountable to management output (1:1 quality, team health, performance feedback)
**What to do:**
- Explicit manager training before promotion (not after)
- Management KPIs separate from IC KPIs
- Peer community for new managers (monthly cohort session)
- HR check-ins on manager health at 30/60/90 days
### Stage 3: Professional Management (80–200 people)
External hires at Director/VP level bring professional management skills but lack company context.
**Common failure modes:**
- Hired "too senior" — VP who's used to 200-person teams in a 50-person function
- Culture clash — Big-company manager who adds process that kills startup speed
- Authority vacuum — External VP doesn't earn trust; team ignores them; founder continues to bypass hierarchy
**Mitigation:**
- Hiring bar: Has this person scaled from this stage to 2x this stage before? Not managed a team at 2x — built a team to 2x.
- Explicit onboarding on "how we make decisions here"
- 90-day milestones focused on relationship-building before any structural changes
- Founders explicitly hand off ownership and reinforce new manager's authority publicly
### Stage 4: Founder Transition from Operator to Executive
The hardest personal transition. Founder moves from doing to enabling.
**Signs you haven't made the transition:**
- You're still in every technical decision
- Teams come to you instead of their manager for approvals
- You know more about the team's work than the manager does
- Managers feel they need to check in before acting
**What the transition requires:**
- Explicit authority delegation in writing (not just verbal)
- Willingness to let managers make decisions you'd make differently
- Redirecting team members to their manager consistently
- Measuring managers on outcomes, not just process adherence
- Letting managers hire and fire without founder override (except final call on VPs)
FILE:references/people_strategy.md
# People Strategy Reference
Hiring, retention, performance, and remote/hybrid frameworks for each growth stage.
---
## Hiring Strategy by Growth Stage
### Pre-Seed / Seed (1–15 people)
**Who you're hiring:** Generalists who can do multiple jobs. Specialists are a luxury you can't afford unless the specialty is your core product.
**The test:** Could this person be the 5th employee at a startup and thrive? If they need a defined role, clear process, and a manager — not yet.
**Sourcing at this stage:**
- Founder networks first (highest signal, lowest cost)
- Angel List / Wellfound — self-selected for startup risk tolerance
- Referrals from existing employees (offer a referral bonus from day 1)
- GitHub / Dribbble / published work for technical roles
- Avoid: Big job boards, recruiters (unless technical retained search for C-suite)
**Interview process (keep it lean):**
1. 30-min intro call (culture/motivation fit, comp alignment)
2. Take-home or live work sample (2–4 hours max, paid for senior roles)
3. 60-min deep-dive with founders
4. Reference checks (3 calls, not emails — you want the real story)
**Offer timeline:** Decision within 48 hours. Top candidates have multiple offers.
**What to get right:**
- Written job scorecard (outcomes expected in 30/60/90 days) — not a job description
- Equity range disclosed in first conversation
- No exploding offers. Pressure tactics lose good people.
---
### Series A (15–50 people)
**The hiring shift:** You need some specialists now. First management layer emerges. First "culture carries" — people who reinforce what you want to become.
**Critical hires at this stage (in priority order):**
1. VP/Head of Engineering (if founder isn't technical)
2. Head of Product
3. First dedicated recruiter (when you're hiring > 10/year)
4. First Finance/Operations hire
5. Head of Sales (when product-market fit is real)
**Building the recruiting function:**
- First recruiter should be a generalist with hustle, not a specialist
- Set up an ATS (Ashby, Greenhouse, or Lever) before you need it — not after
- Create interview scorecards for every role
- Track: time to fill, offer acceptance rate, source quality
**Common mistakes at Series A:**
- Promoting top ICs to management without management training
- Hiring "brand name" executives who've never operated lean
- Over-indexing on experience, under-indexing on trajectory
- No onboarding process → 90-day regrettable turnover
**Job scorecards (required for every role):**
```
Role: [Title]
Reports to: [Manager]
Start date: [Target]
Why this role now: [Business case in 1-2 sentences]
Outcomes (90 days):
- [Concrete deliverable 1]
- [Concrete deliverable 2]
- [Concrete deliverable 3]
Outcomes (12 months):
- [Strategic impact 1]
- [Strategic impact 2]
Competencies (top 3 only):
- [What, why it matters for THIS role]
- [What, why it matters for THIS role]
- [What, why it matters for THIS role]
Comp range: [Base] + [Equity] + [Benefits summary]
```
---
### Series B (50–150 people)
**The scaling inflection point.** Tribal knowledge breaks. Process matters now. Culture requires deliberate investment.
**What changes:**
- Recruiters become specialists (technical, GTM, exec)
- Manager training becomes non-negotiable
- Performance management needs structure (not just "we'll know it when we see it")
- Onboarding needs to scale without founders in every session
- Comp bands become essential — people are comparing notes
**Hiring velocity benchmarks (Series B):**
| Function | Avg time to fill | Avg interviews | Benchmark offer acceptance |
|----------|-----------------|----------------|---------------------------|
| Engineering IC | 35–45 days | 4–5 rounds | 80–85% |
| Engineering Manager | 45–60 days | 5–6 rounds | 75–80% |
| Sales IC | 25–35 days | 3–4 rounds | 85–90% |
| Sales Manager | 40–55 days | 4–5 rounds | 80–85% |
| G&A (Finance, HR, Ops) | 30–45 days | 3–4 rounds | 85–90% |
**Internal mobility:** By 50 people, start tracking internal promotion rates. Target: 20–30% of manager+ roles filled internally. If it's < 10%, your career development is failing.
---
### Series C+ (150+ people)
**Professional management era.** Founders can't know everyone. Systems and culture carry what personal relationships used to.
**HR function maturity required:**
- Dedicated HRBPs per business unit (1:75–100 employees)
- L&D budget (1–2% of salary budget minimum)
- Succession planning for all VP+ roles
- Structured calibration process for performance reviews
- Total rewards strategy reviewed annually with board
---
## Retention Programs That Actually Work
### What drives retention (in order of impact)
1. **Manager quality** — Gallup: 70% of team engagement variance is explained by the manager. Fix managers first.
2. **Growth trajectory** — People leave when they can't see their next role. Career ladders are retention tools.
3. **Compensation competitiveness** — Being at P25 on salary is a slow leak. Audit annually.
4. **Mission/product belief** — Especially for senior ICs. They want to work on something that matters.
5. **Team quality** — "I stay because of the people I work with." True at every level.
6. **Flexibility** — Location, hours, autonomy. Low cost, high impact.
### What doesn't work (but companies do anyway)
- Pizza parties and ping pong tables
- "Perks" that substitute for salary
- Annual reviews with no action on feedback
- Forced fun events
- Vague "culture improvement" initiatives without specific behavior changes
### The 30-60-90 Onboarding Framework
Structured onboarding cuts 90-day turnover by 50%+.
**Days 1–30: Learn**
- Complete admin setup (day 1, before lunch)
- Meet all key stakeholders (scheduled by their manager, not on the new hire)
- Understand: business model, current priorities, team processes, how success is measured
- No deliverables expected. Learning is the job.
- Weekly 1:1 with manager: "What's confusing? What do you need?"
**Days 31–60: Contribute**
- First real project (scoped to be completable)
- Present findings or work to the team
- Identify one process that could be improved (observation only — don't fix yet)
- 30-day check-in: formal feedback from manager
**Days 61–90: Lead**
- Own a deliverable end-to-end
- Offer one specific improvement recommendation with data
- 90-day review: mutual assessment — manager on new hire, new hire on onboarding
- Set 6-month goals
### Stay Interviews (underused, high ROI)
Run with every employee once per year. Not their manager — HR or skip-level.
**Questions that surface real risk:**
- "What's keeping you here?"
- "What would make you consider leaving?"
- "What's one thing your manager could do differently?"
- "Is your role what you expected when you joined?"
- "What career path do you want? Are we helping you get there?"
- "Are you fairly compensated? Do you know how you'd get a raise?"
**Act on answers within 30 days or don't ask.** Unanswered feedback is worse than no feedback.
### Exit Interviews — What to Actually Learn
Skip the happiness survey. Ask these:
- "When did you first think about leaving?"
- "Was there a specific event that triggered your decision?"
- "What could we have done to retain you?"
- "Where are you going and why?" (What does the other offer have that we don't?)
- "Would you recommend us as an employer? Why or why not?"
Track exit themes by manager. If one manager's exits cite "micromanagement" three times — that's data.
---
## Performance Management
### The System That Works
**Continuous > annual.** Annual reviews with no mid-year touchpoints are theater.
**Structure:**
- **Weekly 1:1s** (30 min): blockers, priorities, relationship
- **Monthly check-ins** (1 hr): progress against goals, feedback exchange
- **Quarterly reviews** (formal): written self-assessment + manager assessment + goal revision
- **Annual calibration** (rating + comp): cross-manager calibration session, then individual conversations
### Calibration Sessions
**Purpose:** Prevent manager bias. Ensure "exceeds expectations" means the same thing across teams.
**Process:**
1. Managers submit preliminary ratings independently
2. HR facilitates 2-hr calibration with all managers in a function
3. Managers must justify outliers (top and bottom)
4. Ratings adjusted for consistency
5. Managers deliver final ratings with rationale
**Distribution guidance (enforce with calibration):**
- Exceptional (5): < 10% — if everyone's exceptional, no one is
- Exceeds (4): 20–25%
- Meets (3): 55–65%
- Needs improvement (2): 8–12%
- Underperforming (1): 2–5%
### Managing Underperformers
**The most avoided management task. And the most damaging when avoided.**
High performers notice when underperformers are tolerated. They leave.
**The 4-step framework:**
**Step 1: Diagnose before acting** (Week 1–2)
- Is this a skill gap (can't do it) or a will gap (won't do it)?
- Skill gap → training, clearer expectations, different role
- Will gap → direct feedback, clear consequences, then PIP
**Step 2: Direct feedback conversation** (Week 2–3)
- Specific: "Your last 3 sprint deliveries were 40% incomplete"
- Not: "You're not meeting expectations"
- Document. Send written summary after every feedback conversation.
**Step 3: Performance Improvement Plan (PIP)**
Required when: two rounds of direct feedback haven't produced change.
PIP structure:
```
Name: [Employee]
Manager: [Name]
Date: [Start]
Review date: [30/60 days out]
Current performance issues:
- [Specific, observable behavior with examples and dates]
- [Metric not met: target X, actual Y for Z weeks]
Required improvements:
- [Specific, measurable outcome 1] by [date]
- [Specific, measurable outcome 2] by [date]
Support provided:
- [Training, coaching, additional resources]
Consequences if not met: [Role change / separation]
Check-in schedule: [Weekly with manager + HR]
```
**Step 4: Exit or role change**
- If PIP milestones not met: proceed to separation
- Don't extend PIPs indefinitely — it's unfair to the employee and the team
- Offer a graceful exit where possible: "This role isn't the right fit. Here's a package and a reference."
**What not to do:**
- "Quiet manage out" without clear feedback (legally risky, unfair)
- PIP as a formality before termination (if you know you're firing them, just do it)
- Tolerating underperformance "because we're understaffed" (it makes understaffing worse)
---
## Remote / Hybrid Strategy
### The question isn't "remote or not" — it's "what kind of collaboration does our work require?"
**Work type taxonomy:**
| Work type | Remote-compatible? | Hybrid compatible? |
|-----------|-------------------|-------------------|
| Deep individual work (coding, writing, analysis) | Yes | Yes |
| Async collaboration (code review, doc review) | Yes | Yes |
| Synchronous problem-solving (debugging, design) | Yes (video) | Yes |
| Relationship-building (onboarding, new team) | Harder | Yes |
| Executive alignment, strategy | Harder | Yes — quarterly in-person |
| Sales (enterprise, relationship-based) | No | Depends on market |
### Making Hybrid Work (Not Just a Policy)
**The failure mode:** "Hybrid" = go to office on Tuesday/Thursday, but no one coordinates, all meetings are still Zoom anyway.
**What actually works:**
1. **Anchor days with purpose** — Office days should have things that require the office: workshops, team rituals, whiteboarding sessions. Not just "presence."
2. **Async-first culture, not async-only** — Document decisions. Write things down. Use Loom for walkthroughs. Reduce "quick sync" meetings.
3. **Equal experience for remote participants** — If some are in the room and some are on video, the remote folks are second-class. Either everyone's remote or set up rooms properly.
4. **Manager standards for remote teams:**
- 1:1s are non-negotiable (video, not async)
- Over-communicate on priorities (people can't absorb hallway context)
- Write down decisions (remote employees miss casual office decisions)
- Recognize work publicly (Slack shoutouts, all-hands wins)
### Remote Compensation Philosophy (pick one, be explicit)
**Option A: Location-based pay**
Pay based on where the employee lives. Lower cost in lower-cost markets. Harder to hire in high-cost cities.
**Option B: Role-based (location-neutral)**
One band for each role regardless of location. Simpler, more equitable. Higher overall payroll cost.
**Option C: Zone-based**
Define 2–3 geographic zones (e.g., Tier 1 cities, Tier 2 cities, international). Set bands per zone. Common at mid-stage startups.
**The wrong answer:** No stated policy, and every offer is negotiated individually. Creates pay equity problems fast.
FILE:scripts/comp_benchmarker.py
#!/usr/bin/env python3
"""
Compensation Benchmarker
========================
Salary benchmarking and total comp modeling for startup teams.
Analyzes pay equity, compa-ratios, and total comp vs. market.
Usage:
python comp_benchmarker.py # Run with built-in sample data
python comp_benchmarker.py --config roster.json # Load from JSON
python comp_benchmarker.py --help
Output: Band compliance report, compa-ratio distribution, pay equity flags,
equity value analysis, and total comp vs. market.
"""
import argparse
import json
import csv
import io
import sys
from dataclasses import dataclass, field, asdict
from typing import Optional
from datetime import date
import math
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
@dataclass
class BandDefinition:
"""Salary band for a role level."""
level: str # L1, L2, L3, L4, M1, M2, M3, VP
function: str # Engineering, Sales, Product, G&A, Marketing, CS
band_min: int # Annual USD
band_mid: int # P50 anchor
band_max: int # Band ceiling
market_p25: int # Market 25th percentile
market_p50: int # Market median (should align with band_mid for P50 strategy)
market_p75: int # Market 75th percentile
location_zone: str # Tier1 (SF/NYC), Tier2 (Austin/Denver), Tier3 (Remote/other), EU
@dataclass
class Employee:
"""One employee record."""
id: str
name: str
role: str
level: str
function: str
location_zone: str
base_salary: int
bonus_target_pct: float # % of base
equity_shares: int # Total unvested options/RSUs
equity_strike: float # Strike price (0 for RSUs)
equity_current_409a: float # Current 409A share price
equity_vest_years_remaining: float # How many years of vesting remain
benefits_annual: int # Employer-paid benefits cost
gender: str # M/F/NB/Undisclosed (for equity audit)
ethnicity: str # For equity audit — can be "Undisclosed"
tenure_years: float
performance_rating: int # 1–5
last_raise_months_ago: int
last_equity_refresh_months_ago: Optional[int] = None
@dataclass
class CompRoster:
company: str
as_of_date: str # ISO date
funding_stage: str # Seed, Series A, Series B, etc.
comp_philosophy_target: str # P50, P65, P75 — your target percentile
preferred_stock_price: float # Last round price (for offer modeling)
employees: list[Employee] = field(default_factory=list)
bands: list[BandDefinition] = field(default_factory=list)
# ---------------------------------------------------------------------------
# Band lookup
# ---------------------------------------------------------------------------
def find_band(roster: CompRoster, level: str, function: str, zone: str) -> Optional[BandDefinition]:
"""Find best-matching band. Falls back to any matching level+function if zone not found."""
matches = [b for b in roster.bands if b.level == level and b.function == function and b.location_zone == zone]
if matches:
return matches[0]
# Fallback: same level+function, any zone
matches = [b for b in roster.bands if b.level == level and b.function == function]
if matches:
return matches[0]
# Fallback: same level, any function
matches = [b for b in roster.bands if b.level == level]
if matches:
return matches[0]
return None
# ---------------------------------------------------------------------------
# Compensation analysis
# ---------------------------------------------------------------------------
def compa_ratio(salary: int, band_mid: int) -> float:
return salary / band_mid if band_mid > 0 else 0.0
def band_position(salary: int, band_min: int, band_max: int) -> float:
"""Position in band: 0.0 = at min, 1.0 = at max."""
if band_max == band_min:
return 0.5
return (salary - band_min) / (band_max - band_min)
def annualized_equity_value(emp: Employee) -> int:
"""Current 409A value of unvested equity, annualized."""
if emp.equity_vest_years_remaining <= 0:
return 0
if emp.equity_current_409a > emp.equity_strike:
intrinsic = (emp.equity_current_409a - emp.equity_strike) * emp.equity_shares
else:
# Options underwater — still show at current FMV for RSUs or future value for options
intrinsic = emp.equity_current_409a * emp.equity_shares if emp.equity_strike == 0 else 0
return int(intrinsic / emp.equity_vest_years_remaining)
def total_comp(emp: Employee) -> int:
bonus = int(emp.base_salary * emp.bonus_target_pct)
equity = annualized_equity_value(emp)
return emp.base_salary + bonus + equity + emp.benefits_annual
def analyze_employee(emp: Employee, roster: CompRoster) -> dict:
band = find_band(roster, emp.level, emp.function, emp.location_zone)
result = {
"id": emp.id,
"name": emp.name,
"role": emp.role,
"level": emp.level,
"function": emp.function,
"zone": emp.location_zone,
"base": emp.base_salary,
"bonus_target": int(emp.base_salary * emp.bonus_target_pct),
"equity_annual": annualized_equity_value(emp),
"benefits": emp.benefits_annual,
"total_comp": total_comp(emp),
"performance": emp.performance_rating,
"tenure_years": emp.tenure_years,
"last_raise_months": emp.last_raise_months_ago,
"band": band,
"compa_ratio": None,
"band_position": None,
"vs_market_p50": None,
"flags": [],
}
if band:
cr = compa_ratio(emp.base_salary, band.band_mid)
bp = band_position(emp.base_salary, band.band_min, band.band_max)
result["compa_ratio"] = round(cr, 3)
result["band_position"] = round(bp, 3)
result["vs_market_p50"] = round((emp.base_salary - band.market_p50) / band.market_p50 * 100, 1)
# Flags
if emp.base_salary < band.band_min:
result["flags"].append(("CRITICAL", "Base below band minimum — immediate attrition risk"))
elif cr < 0.88:
result["flags"].append(("HIGH", f"Compa-ratio {cr:.2f} — significantly below midpoint"))
elif cr < 0.93:
result["flags"].append(("MEDIUM", f"Compa-ratio {cr:.2f} — below target zone (0.95–1.05)"))
if emp.base_salary > band.band_max:
result["flags"].append(("HIGH", "Base above band maximum — review for promotion or band update"))
if emp.performance_rating >= 4 and cr < 0.95:
result["flags"].append(("HIGH", f"High performer (rating {emp.performance_rating}) underpaid — flight risk"))
if emp.last_raise_months_ago > 18:
result["flags"].append(("MEDIUM", f"No raise in {emp.last_raise_months_ago} months — review due"))
if emp.equity_vest_years_remaining < 1.0 and (emp.last_equity_refresh_months_ago is None or emp.last_equity_refresh_months_ago > 24):
result["flags"].append(("HIGH", "Equity nearly fully vested with no refresh — retention hook gone"))
else:
result["flags"].append(("INFO", "No band found for this level/function/zone"))
return result
# ---------------------------------------------------------------------------
# Aggregate analysis
# ---------------------------------------------------------------------------
def pay_equity_audit(analyses: list[dict], employees: list[Employee]) -> dict:
"""Simple pay equity analysis by gender and ethnicity."""
emp_by_id = {e.id: e for e in employees}
def group_stats(group_key_fn):
groups: dict[str, list[float]] = {}
for a in analyses:
if a["compa_ratio"] is None:
continue
emp = emp_by_id.get(a["id"])
if not emp:
continue
key = group_key_fn(emp)
if key not in groups:
groups[key] = []
groups[key].append(a["compa_ratio"])
return {k: {"n": len(v), "avg_cr": round(sum(v)/len(v), 3), "min_cr": round(min(v), 3), "max_cr": round(max(v), 3)}
for k, v in groups.items() if v}
gender_stats = group_stats(lambda e: e.gender)
ethnicity_stats = group_stats(lambda e: e.ethnicity)
# Compute gap vs. the largest group
def compute_gap(stats: dict) -> dict[str, float]:
if not stats:
return {}
largest = max(stats.items(), key=lambda x: x[1]["n"])
ref_cr = largest[1]["avg_cr"]
return {k: round((v["avg_cr"] - ref_cr) / ref_cr * 100, 1) for k, v in stats.items()}
gender_gaps = compute_gap(gender_stats)
ethnicity_gaps = compute_gap(ethnicity_stats)
return {
"gender": gender_stats,
"gender_gaps_pct": gender_gaps,
"ethnicity": ethnicity_stats,
"ethnicity_gaps_pct": ethnicity_gaps,
}
def compa_ratio_distribution(analyses: list[dict]) -> dict:
crs = [a["compa_ratio"] for a in analyses if a["compa_ratio"] is not None]
if not crs:
return {}
buckets = {
"< 0.85 (below band)": 0,
"0.85–0.94 (developing)": 0,
"0.95–1.05 (target zone)": 0,
"1.06–1.15 (senior in role)": 0,
"> 1.15 (above band)": 0,
}
for cr in crs:
if cr < 0.85:
buckets["< 0.85 (below band)"] += 1
elif cr < 0.95:
buckets["0.85–0.94 (developing)"] += 1
elif cr <= 1.05:
buckets["0.95–1.05 (target zone)"] += 1
elif cr <= 1.15:
buckets["1.06–1.15 (senior in role)"] += 1
else:
buckets["> 1.15 (above band)"] += 1
avg = sum(crs) / len(crs)
return {"distribution": buckets, "avg_compa_ratio": round(avg, 3), "n": len(crs)}
# ---------------------------------------------------------------------------
# Report output
# ---------------------------------------------------------------------------
def fmt(n) -> str:
return f",.0f"
def bar(value: float, width: int = 20) -> str:
filled = min(width, max(0, int(value * width)))
return "█" * filled + "░" * (width - filled)
def print_report(roster: CompRoster):
WIDTH = 76
SEP = "=" * WIDTH
sep = "-" * WIDTH
analyses = [analyze_employee(e, roster) for e in roster.employees]
cr_dist = compa_ratio_distribution(analyses)
equity_audit = pay_equity_audit(analyses, roster.employees)
print(SEP)
print(f" COMPENSATION BENCHMARKING REPORT — {roster.company}")
print(f" As of: {roster.as_of_date} | Stage: {roster.funding_stage} | Target: {roster.comp_philosophy_target}")
print(SEP)
# Summary stats
total_emps = len(roster.employees)
flagged = sum(1 for a in analyses if any(s in ["CRITICAL", "HIGH"] for s, _ in a["flags"]))
total_payroll = sum(e.base_salary for e in roster.employees)
avg_total_comp = sum(a["total_comp"] for a in analyses) // total_emps if total_emps else 0
print(f"\n[ SUMMARY ]")
print(sep)
print(f" Employees analyzed: {total_emps}")
print(f" Flagged (critical/high): {flagged}")
print(f" Total base payroll: {fmt(total_payroll)}/year")
print(f" Avg total comp: {fmt(avg_total_comp)}/year")
if cr_dist:
print(f" Avg compa-ratio: {cr_dist['avg_compa_ratio']:.3f}")
# Compa-ratio distribution
if cr_dist:
print(f"\n[ COMPA-RATIO DISTRIBUTION ]")
print(sep)
total_n = cr_dist["n"]
for label, count in cr_dist["distribution"].items():
pct = count / total_n if total_n else 0
bar_str = bar(pct, 25)
print(f" {label:<30} {bar_str} {count:3d} ({pct*100:4.0f}%)")
# Pay equity audit
print(f"\n[ PAY EQUITY AUDIT ]")
print(sep)
print(f" By Gender:")
for group, stats in equity_audit["gender"].items():
gap = equity_audit["gender_gaps_pct"].get(group, 0.0)
gap_str = f" gap: {gap:+.1f}%" if gap != 0 else " (reference group)"
flag = " ⚠" if abs(gap) > 5 else ""
print(f" {group:<15} n={stats['n']} avg_CR={stats['avg_cr']:.3f}{gap_str}{flag}")
print(f"\n By Ethnicity:")
for group, stats in equity_audit["ethnicity"].items():
gap = equity_audit["ethnicity_gaps_pct"].get(group, 0.0)
gap_str = f" gap: {gap:+.1f}%" if gap != 0 else " (reference group)"
flag = " ⚠" if abs(gap) > 5 else ""
print(f" {group:<20} n={stats['n']} avg_CR={stats['avg_cr']:.3f}{gap_str}{flag}")
print(f"\n ⚠ = gap > 5%. Investigate with regression controlling for level, tenure, and performance.")
# Employee detail with flags
print(f"\n[ EMPLOYEE DETAIL ]")
print(sep)
# Group by function
functions = sorted(set(e.function for e in roster.employees))
for fn in functions:
fn_analyses = [a for a in analyses if a["function"] == fn]
if not fn_analyses:
continue
print(f"\n ── {fn} ──")
print(f" {'Name':<22} {'Role':<28} {'Lvl':<5} {'Base':>10} {'TotalComp':>11} {'CR':>6} {'Perf':>5} Flags")
print(f" {'-'*22} {'-'*28} {'-'*5} {'-'*10} {'-'*11} {'-'*6} {'-'*5} {'-'*20}")
for a in sorted(fn_analyses, key=lambda x: -x["base"]):
cr_str = f"{a['compa_ratio']:.2f}" if a["compa_ratio"] else "N/A"
flag_summary = ", ".join(s for s, _ in a["flags"] if s in ("CRITICAL", "HIGH", "MEDIUM"))
flag_str = flag_summary if flag_summary else "OK"
print(f" {a['name']:<22} {a['role']:<28} {a['level']:<5} "
f"{fmt(a['base']):>10} {fmt(a['total_comp']):>11} {cr_str:>6} {a['performance']:>5} {flag_str}")
# Print flag detail for critical/high
for severity, msg in a["flags"]:
if severity in ("CRITICAL", "HIGH"):
print(f" {'':>22} ↳ [{severity}] {msg}")
# Action items
critical = [(a["name"], msg) for a in analyses for sev, msg in a["flags"] if sev == "CRITICAL"]
high = [(a["name"], msg) for a in analyses for sev, msg in a["flags"] if sev == "HIGH"]
medium = [(a["name"], msg) for a in analyses for sev, msg in a["flags"] if sev == "MEDIUM"]
print(f"\n[ ACTION ITEMS ]")
print(sep)
if critical:
print(f"\n CRITICAL — Address this review cycle:")
for name, msg in critical:
print(f" • {name}: {msg}")
if high:
print(f"\n HIGH — Address within 30 days:")
for name, msg in high[:10]:
print(f" • {name}: {msg}")
if len(high) > 10:
print(f" ... and {len(high)-10} more")
if medium:
print(f"\n MEDIUM — Address in next comp cycle:")
for name, msg in medium[:8]:
print(f" • {name}: {msg}")
if len(medium) > 8:
print(f" ... and {len(medium)-8} more")
if not critical and not high and not medium:
print(f"\n No critical or high-severity issues. Compensation appears well-managed.")
# Remediation cost estimate
below_min = [a for a in analyses if a["band"] and a["base"] < a["band"].band_min]
below_mid = [a for a in analyses if a["compa_ratio"] and a["compa_ratio"] < 0.90]
if below_min or below_mid:
print(f"\n[ REMEDIATION COST ESTIMATE ]")
print(sep)
if below_min:
cost_to_min = sum(a["band"].band_min - a["base"] for a in below_min)
print(f" Cost to bring below-minimum to band min: {fmt(cost_to_min)}/year ({len(below_min)} employees)")
if below_mid:
cost_to_90 = sum(int(a["band"].band_mid * 0.90) - a["base"] for a in below_mid if a["base"] < int(a["band"].band_mid * 0.90))
cost_to_90 = max(0, cost_to_90)
print(f" Cost to bring CR < 0.90 to CR = 0.90: {fmt(cost_to_90)}/year ({len(below_mid)} employees)")
total_payroll_impact = sum(e.base_salary for e in roster.employees)
total_remediation = (below_min and cost_to_min or 0)
print(f"\n Total payroll before remediation: {fmt(total_payroll_impact)}/year")
print(f" Remediation as % of payroll: {total_remediation/total_payroll_impact*100:.1f}%")
print(f"\n{SEP}\n")
def export_csv(roster: CompRoster) -> str:
analyses = [analyze_employee(e, roster) for e in roster.employees]
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["ID", "Name", "Role", "Level", "Function", "Zone",
"Base", "Bonus Target", "Equity Annual", "Benefits", "Total Comp",
"Compa Ratio", "Band Position", "vs Market P50 %",
"Performance", "Tenure Years", "Last Raise (mo)",
"Gender", "Ethnicity", "Critical Flags", "High Flags"])
for a, e in zip(analyses, roster.employees):
critical_flags = "; ".join(msg for sev, msg in a["flags"] if sev == "CRITICAL")
high_flags = "; ".join(msg for sev, msg in a["flags"] if sev == "HIGH")
writer.writerow([a["id"], a["name"], a["role"], a["level"], a["function"], a["zone"],
a["base"], a["bonus_target"], a["equity_annual"], a["benefits"], a["total_comp"],
a["compa_ratio"], a["band_position"], a["vs_market_p50"],
a["performance"], a["tenure_years"], a["last_raise_months"],
e.gender, e.ethnicity, critical_flags, high_flags])
return output.getvalue()
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
def build_sample_roster() -> CompRoster:
roster = CompRoster(
company="AcmeTech (Series A)",
as_of_date=date.today().isoformat(),
funding_stage="Series A",
comp_philosophy_target="P50",
preferred_stock_price=8.50,
)
# Bands (Engineering, P50 target, Tier1 = SF/NYC)
roster.bands = [
BandDefinition("L2", "Engineering", 115_000, 132_000, 155_000, 110_000, 132_000, 155_000, "Tier1"),
BandDefinition("L3", "Engineering", 148_000, 170_000, 198_000, 145_000, 170_000, 198_000, "Tier1"),
BandDefinition("L4", "Engineering", 185_000, 215_000, 248_000, 182_000, 215_000, 250_000, "Tier1"),
BandDefinition("M1", "Engineering", 170_000, 195_000, 225_000, 168_000, 195_000, 225_000, "Tier1"),
BandDefinition("L2", "Engineering", 95_000, 108_000, 125_000, 92_000, 108_000, 126_000, "Tier2"),
BandDefinition("L3", "Engineering", 122_000, 140_000, 162_000, 120_000, 140_000, 162_000, "Tier2"),
BandDefinition("L2", "Sales", 80_000, 92_000, 108_000, 78_000, 92_000, 108_000, "Tier1"),
BandDefinition("L3", "Sales", 95_000, 110_000, 128_000, 93_000, 110_000, 128_000, "Tier1"),
BandDefinition("M1", "Sales", 130_000, 150_000, 172_000, 128_000, 150_000, 172_000, "Tier1"),
BandDefinition("L2", "Product", 125_000, 145_000, 168_000, 123_000, 145_000, 168_000, "Tier1"),
BandDefinition("L3", "Product", 155_000, 178_000, 205_000, 153_000, 178_000, 205_000, "Tier1"),
BandDefinition("L2", "G&A", 85_000, 98_000, 115_000, 83_000, 98_000, 115_000, "Tier1"),
BandDefinition("L3", "G&A", 110_000, 128_000, 148_000, 108_000, 128_000, 148_000, "Tier1"),
]
roster.employees = [
# Engineering — mix of scenarios
Employee("E001", "Aarav Shah", "Senior SWE (Backend)", "L3", "Engineering", "Tier1",
base_salary=168_000, bonus_target_pct=0.0, equity_shares=40_000,
equity_strike=1.50, equity_current_409a=6.80, equity_vest_years_remaining=2.5,
benefits_annual=18_000, gender="M", ethnicity="Asian",
tenure_years=2.5, performance_rating=4, last_raise_months_ago=14,
last_equity_refresh_months_ago=None),
Employee("E002", "Yuki Tanaka", "Senior SWE (Frontend)", "L3", "Engineering", "Tier1",
base_salary=152_000, bonus_target_pct=0.0, equity_shares=30_000,
equity_strike=2.20, equity_current_409a=6.80, equity_vest_years_remaining=0.5,
benefits_annual=18_000, gender="F", ethnicity="Asian",
tenure_years=3.8, performance_rating=5, last_raise_months_ago=11,
last_equity_refresh_months_ago=30),
# Note: Yuki is high performer, near-vested, no recent refresh — flag expected
Employee("E003", "Marcus Johnson", "SWE II (Backend)", "L2", "Engineering", "Tier1",
base_salary=110_000, bonus_target_pct=0.0, equity_shares=15_000,
equity_strike=2.50, equity_current_409a=6.80, equity_vest_years_remaining=3.0,
benefits_annual=15_000, gender="M", ethnicity="Black",
tenure_years=1.2, performance_rating=3, last_raise_months_ago=12,
last_equity_refresh_months_ago=None),
# Note: Below band midpoint, recently hired — developing flag
Employee("E004", "Priya Nair", "Staff SWE", "L4", "Engineering", "Tier1",
base_salary=222_000, bonus_target_pct=0.0, equity_shares=60_000,
equity_strike=0.80, equity_current_409a=6.80, equity_vest_years_remaining=2.0,
benefits_annual=18_000, gender="F", ethnicity="Asian",
tenure_years=4.2, performance_rating=5, last_raise_months_ago=8,
last_equity_refresh_months_ago=8),
Employee("E005", "Tom Rivera", "SWE II (Platform)", "L2", "Engineering", "Tier2",
base_salary=88_000, bonus_target_pct=0.0, equity_shares=12_000,
equity_strike=3.00, equity_current_409a=6.80, equity_vest_years_remaining=2.5,
benefits_annual=14_000, gender="M", ethnicity="Hispanic",
tenure_years=1.8, performance_rating=4, last_raise_months_ago=22,
last_equity_refresh_months_ago=None),
# Note: No raise in 22 months, high performer — flag expected
Employee("E006", "Sarah Kim", "Eng Manager", "M1", "Engineering", "Tier1",
base_salary=192_000, bonus_target_pct=0.10, equity_shares=35_000,
equity_strike=1.20, equity_current_409a=6.80, equity_vest_years_remaining=1.8,
benefits_annual=18_000, gender="F", ethnicity="Asian",
tenure_years=2.8, performance_rating=4, last_raise_months_ago=9,
last_equity_refresh_months_ago=9),
# Sales
Employee("S001", "David Chen", "Account Executive (MM)", "L3", "Sales", "Tier1",
base_salary=105_000, bonus_target_pct=0.50, equity_shares=8_000,
equity_strike=3.50, equity_current_409a=6.80, equity_vest_years_remaining=2.0,
benefits_annual=15_000, gender="M", ethnicity="Asian",
tenure_years=1.5, performance_rating=3, last_raise_months_ago=15,
last_equity_refresh_months_ago=None),
Employee("S002", "Amara Osei", "AE (Mid-Market)", "L3", "Sales", "Tier1",
base_salary=98_000, bonus_target_pct=0.50, equity_shares=6_000,
equity_strike=3.50, equity_current_409a=6.80, equity_vest_years_remaining=2.5,
benefits_annual=15_000, gender="F", ethnicity="Black",
tenure_years=1.0, performance_rating=4, last_raise_months_ago=12,
last_equity_refresh_months_ago=None),
# Note: High performer, significantly below midpoint — flag expected
Employee("S003", "Jordan Blake", "Sales Manager", "M1", "Sales", "Tier1",
base_salary=155_000, bonus_target_pct=0.20, equity_shares=20_000,
equity_strike=2.00, equity_current_409a=6.80, equity_vest_years_remaining=1.5,
benefits_annual=16_000, gender="NB", ethnicity="White",
tenure_years=2.2, performance_rating=3, last_raise_months_ago=10,
last_equity_refresh_months_ago=10),
# Product
Employee("P001", "Nina Patel", "Senior PM", "L3", "Product", "Tier1",
base_salary=176_000, bonus_target_pct=0.10, equity_shares=22_000,
equity_strike=1.80, equity_current_409a=6.80, equity_vest_years_remaining=2.0,
benefits_annual=17_000, gender="F", ethnicity="Asian",
tenure_years=2.0, performance_rating=4, last_raise_months_ago=12,
last_equity_refresh_months_ago=12),
# G&A
Employee("G001", "Chris Mueller", "Finance Manager", "L3", "G&A", "Tier1",
base_salary=125_000, bonus_target_pct=0.10, equity_shares=10_000,
equity_strike=2.80, equity_current_409a=6.80, equity_vest_years_remaining=3.0,
benefits_annual=16_000, gender="M", ethnicity="White",
tenure_years=1.5, performance_rating=3, last_raise_months_ago=15,
last_equity_refresh_months_ago=None),
Employee("G002", "Fatima Al-Hassan", "HR Operations", "L2", "G&A", "Tier1",
base_salary=82_000, bonus_target_pct=0.08, equity_shares=5_000,
equity_strike=4.00, equity_current_409a=6.80, equity_vest_years_remaining=3.5,
benefits_annual=14_000, gender="F", ethnicity="Middle Eastern",
tenure_years=0.8, performance_rating=3, last_raise_months_ago=8,
last_equity_refresh_months_ago=None),
# Note: Below band minimum — critical flag expected
]
return roster
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def load_roster_from_json(path: str) -> CompRoster:
with open(path) as f:
data = json.load(f)
employees = [Employee(**e) for e in data.pop("employees", [])]
bands = [BandDefinition(**b) for b in data.pop("bands", [])]
roster = CompRoster(**data)
roster.employees = employees
roster.bands = bands
return roster
def main():
parser = argparse.ArgumentParser(
description="Compensation Benchmarker — salary analysis and pay equity audit",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python comp_benchmarker.py # Run sample roster
python comp_benchmarker.py --config roster.json # Load from JSON
python comp_benchmarker.py --export-csv # Output CSV
python comp_benchmarker.py --export-json # Output JSON template
"""
)
parser.add_argument("--config", help="Path to JSON roster file")
parser.add_argument("--export-csv", action="store_true", help="Export analysis as CSV")
parser.add_argument("--export-json", action="store_true", help="Export sample roster as JSON template")
args = parser.parse_args()
if args.config:
roster = load_roster_from_json(args.config)
else:
roster = build_sample_roster()
if args.export_json:
data = asdict(roster)
print(json.dumps(data, indent=2))
return
if args.export_csv:
print(export_csv(roster))
return
print_report(roster)
if __name__ == "__main__":
main()
FILE:scripts/hiring_plan_modeler.py
#!/usr/bin/env python3
"""
Hiring Plan Modeler
===================
Builds hiring plans from business goals with cost projections.
Outputs quarterly headcount plan, cost model, and risk assessment.
Usage:
python hiring_plan_modeler.py # Run with built-in sample data
python hiring_plan_modeler.py --config plan.json # Load from JSON config
python hiring_plan_modeler.py --help
"""
import argparse
import json
import sys
from dataclasses import dataclass, field, asdict
from datetime import datetime, date
from typing import Optional
import csv
import io
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
@dataclass
class HireTarget:
"""One planned hire."""
role: str
level: str # L1, L2, L3, L4, M1, M2, M3, VP, C-Suite
function: str # Engineering, Sales, Product, G&A, Marketing, CS
quarter: str # Q1-2025, Q2-2025, etc.
base_salary: int # Annual, USD
bonus_pct: float # % of base (e.g., 0.10 for 10%)
equity_annual_usd: int # Annualized equity value at current 409A
benefits_annual: int # Employer-paid benefits
recruiter_fee_pct: float= 0.20 # Agency fee if used (0 for internal recruiter)
ramp_months: int = 3 # Months to full productivity
priority: str = "High" # High / Medium / Low
business_case: str = ""
open_to_internal: bool = False
@dataclass
class HiringPlan:
company: str
plan_period: str # e.g., "2025 Annual"
current_headcount: int
target_revenue: int # Annual target revenue ($)
current_revenue: int # Current ARR ($)
hires: list[HireTarget] = field(default_factory=list)
# Cost overheads beyond comp
overhead_rate: float = 0.25 # Workspace, software, onboarding overhead as % of base
internal_recruiter_cost: int = 0 # If you have an internal recruiter, annual cost
# ---------------------------------------------------------------------------
# Computation
# ---------------------------------------------------------------------------
def quarter_to_sortkey(q: str) -> tuple[int, int]:
"""Parse 'Q2-2025' → (2025, 2)"""
parts = q.upper().split("-")
if len(parts) == 2:
q_num = int(parts[0].replace("Q", ""))
year = int(parts[1])
return (year, q_num)
return (9999, 9)
def get_quarters(hires: list[HireTarget]) -> list[str]:
"""Return sorted unique quarters from hire list."""
quarters = sorted(set(h.quarter for h in hires), key=quarter_to_sortkey)
return quarters
def compute_hire_costs(hire: HireTarget) -> dict:
"""Compute total first-year cost for one hire."""
total_comp = hire.base_salary + int(hire.base_salary * hire.bonus_pct) + hire.equity_annual_usd + hire.benefits_annual
recruiter_fee = int(hire.base_salary * hire.recruiter_fee_pct)
overhead = int(hire.base_salary * 0.25) # workspace, tools, onboarding
ramp_productivity_cost = int(hire.base_salary * (hire.ramp_months / 12)) # cost during ramp
return {
"base_salary": hire.base_salary,
"target_bonus": int(hire.base_salary * hire.bonus_pct),
"equity_annual": hire.equity_annual_usd,
"benefits": hire.benefits_annual,
"total_comp": total_comp,
"recruiter_fee": recruiter_fee,
"overhead": overhead,
"ramp_cost": ramp_productivity_cost,
"first_year_total": total_comp + recruiter_fee + overhead,
"fully_loaded_first_year": total_comp + recruiter_fee + overhead + ramp_productivity_cost,
}
def summarize_by_quarter(plan: HiringPlan) -> dict[str, dict]:
"""Aggregate headcount and costs per quarter."""
quarters = get_quarters(plan.hires)
summary = {}
running_headcount = plan.current_headcount
for q in quarters:
q_hires = [h for h in plan.hires if h.quarter == q]
q_costs = [compute_hire_costs(h) for h in q_hires]
total_comp = sum(c["total_comp"] for c in q_costs)
total_first_year = sum(c["first_year_total"] for c in q_costs)
recruiter_fees = sum(c["recruiter_fee"] for c in q_costs)
running_headcount += len(q_hires)
summary[q] = {
"new_hires": len(q_hires),
"headcount_eop": running_headcount,
"total_annual_comp_added": total_comp,
"total_first_year_cost": total_first_year,
"recruiter_fees": recruiter_fees,
"hires": q_hires,
"costs": q_costs,
}
return summary
def summarize_by_function(plan: HiringPlan) -> dict[str, dict]:
"""Aggregate headcount and costs per function."""
functions: dict[str, dict] = {}
for hire in plan.hires:
fn = hire.function
if fn not in functions:
functions[fn] = {"count": 0, "total_comp": 0, "total_first_year": 0, "roles": []}
costs = compute_hire_costs(hire)
functions[fn]["count"] += 1
functions[fn]["total_comp"] += costs["total_comp"]
functions[fn]["total_first_year"] += costs["first_year_total"]
functions[fn]["roles"].append(hire.role)
return functions
def compute_totals(plan: HiringPlan) -> dict:
all_costs = [compute_hire_costs(h) for h in plan.hires]
total_hires = len(plan.hires)
total_comp = sum(c["total_comp"] for c in all_costs)
total_first_year = sum(c["first_year_total"] for c in all_costs)
total_fully_loaded = sum(c["fully_loaded_first_year"] for c in all_costs)
total_recruiter = sum(c["recruiter_fee"] for c in all_costs)
final_headcount = plan.current_headcount + total_hires
revenue_per_employee = plan.target_revenue / final_headcount if final_headcount > 0 else 0
revenue_per_employee_current = plan.current_revenue / plan.current_headcount if plan.current_headcount > 0 else 0
return {
"total_hires": total_hires,
"final_headcount": final_headcount,
"headcount_growth_pct": ((final_headcount - plan.current_headcount) / plan.current_headcount * 100) if plan.current_headcount > 0 else 0,
"total_annual_comp_added": total_comp,
"total_first_year_cost": total_first_year,
"total_fully_loaded_first_year": total_fully_loaded,
"total_recruiter_fees": total_recruiter,
"revenue_per_employee_target": revenue_per_employee,
"revenue_per_employee_current": revenue_per_employee_current,
"avg_comp_per_hire": total_comp // total_hires if total_hires > 0 else 0,
}
# ---------------------------------------------------------------------------
# Risk assessment
# ---------------------------------------------------------------------------
def assess_risks(plan: HiringPlan, totals: dict) -> list[dict]:
risks = []
# Headcount growth too fast
growth_pct = totals["headcount_growth_pct"]
if growth_pct > 80:
risks.append({
"severity": "HIGH",
"category": "Execution",
"finding": f"Headcount growing {growth_pct:.0f}% this period. "
"Culture and processes rarely scale this fast without breakage.",
"recommendation": "Stagger Q3/Q4 hires. Validate Q1/Q2 cohort is onboarded before next wave."
})
elif growth_pct > 50:
risks.append({
"severity": "MEDIUM",
"category": "Execution",
"finding": f"Headcount growing {growth_pct:.0f}% — significant scaling challenge.",
"recommendation": "Ensure onboarding infrastructure scales. Assign buddy/mentor to each hire."
})
# High concentration in one quarter
quarters = get_quarters(plan.hires)
q_counts = {q: sum(1 for h in plan.hires if h.quarter == q) for q in quarters}
max_q = max(q_counts.values()) if q_counts else 0
if max_q > len(plan.hires) * 0.5 and max_q > 4:
heavy_q = [q for q, c in q_counts.items() if c == max_q][0]
risks.append({
"severity": "MEDIUM",
"category": "Hiring Execution",
"finding": f"More than 50% of hires planned in {heavy_q} ({max_q} hires). "
"Recruiting capacity and onboarding bandwidth may be insufficient.",
"recommendation": "Spread hires across quarters. Hiring pipeline needs to start 60–90 days before target start date."
})
# Revenue per employee declining
if totals["revenue_per_employee_target"] < totals["revenue_per_employee_current"] * 0.7:
risks.append({
"severity": "HIGH",
"category": "Financial",
"finding": f"Revenue per employee declining from ,.0f to "
f",.0f — a {((totals['revenue_per_employee_target']/totals['revenue_per_employee_current'])-1)*100:.0f}% drop.",
"recommendation": "Validate that revenue model supports this headcount. Is target revenue achievable with this team?"
})
# Low priority hires consuming budget
low_priority_hires = [h for h in plan.hires if h.priority == "Low"]
if low_priority_hires:
lp_cost = sum(compute_hire_costs(h)["first_year_total"] for h in low_priority_hires)
risks.append({
"severity": "MEDIUM",
"category": "Prioritization",
"finding": f"{len(low_priority_hires)} 'Low' priority hires consuming ,.0f in first-year costs.",
"recommendation": "Consider deferring Low priority hires to preserve runway. Cut these first if budget tightens."
})
# Hires without business cases
no_case = [h for h in plan.hires if not h.business_case]
if no_case:
risks.append({
"severity": "MEDIUM",
"category": "Governance",
"finding": f"{len(no_case)} hires have no documented business case: {', '.join(h.role for h in no_case[:5])}{'...' if len(no_case) > 5 else ''}",
"recommendation": "Every hire over $80K should have a written business case. What revenue or risk does this role address?"
})
# High recruiter fee exposure
if totals["total_recruiter_fees"] > 100_000:
risks.append({
"severity": "LOW",
"category": "Cost",
"finding": f",.0f in recruiter fees. "
"Consider whether internal recruiter investment would be cheaper at this hiring volume.",
"recommendation": f"Internal recruiter at $120–150K fully loaded pays off at 3–4 hires/year vs. agency fees."
})
# No risks — that's itself a flag
if not risks:
risks.append({
"severity": "INFO",
"category": "General",
"finding": "No major risks flagged. Plan appears well-structured.",
"recommendation": "Validate assumptions: time-to-fill estimates, revenue model, and Q1 hiring pipeline status."
})
return risks
# ---------------------------------------------------------------------------
# Formatting / Output
# ---------------------------------------------------------------------------
def fmt(n: int) -> str:
return f",.0f"
def pct(n: float) -> str:
return f"{n:.1f}%"
def print_report(plan: HiringPlan):
WIDTH = 72
SEP = "=" * WIDTH
sep = "-" * WIDTH
print(SEP)
print(f" HIRING PLAN: {plan.company}")
print(f" Period: {plan.plan_period} | Generated: {date.today().isoformat()}")
print(SEP)
totals = compute_totals(plan)
q_summary = summarize_by_quarter(plan)
fn_summary = summarize_by_function(plan)
risks = assess_risks(plan, totals)
# Executive summary
print("\n[ EXECUTIVE SUMMARY ]")
print(sep)
print(f" Current headcount: {plan.current_headcount:>5}")
print(f" Planned hires: {totals['total_hires']:>5}")
print(f" Final headcount: {totals['final_headcount']:>5} (+{totals['headcount_growth_pct']:.0f}%)")
print(f" Current ARR: {fmt(plan.current_revenue):>12}")
print(f" Target revenue: {fmt(plan.target_revenue):>12}")
print(f" Revenue/employee now: {fmt(int(totals['revenue_per_employee_current'])):>12}")
print(f" Revenue/employee target: {fmt(int(totals['revenue_per_employee_target'])):>12}")
print()
print(f" Total annual comp added: {fmt(totals['total_annual_comp_added']):>12}")
print(f" Total first-year cost: {fmt(totals['total_first_year_cost']):>12}")
print(f" Fully loaded (w/ ramp): {fmt(totals['total_fully_loaded_first_year']):>12}")
print(f" Recruiter fees: {fmt(totals['total_recruiter_fees']):>12}")
print(f" Avg comp per hire: {fmt(totals['avg_comp_per_hire']):>12}")
# Quarterly breakdown
print(f"\n[ QUARTERLY HEADCOUNT PLAN ]")
print(sep)
print(f" {'Quarter':<10} {'New Hires':>10} {'HC (EOP)':>10} {'Comp Added':>14} {'1yr Cost':>14} {'Recruiter $':>12}")
print(f" {'-'*10} {'-'*10} {'-'*10} {'-'*14} {'-'*14} {'-'*12}")
for q, data in q_summary.items():
print(f" {q:<10} {data['new_hires']:>10} {data['headcount_eop']:>10} "
f"{fmt(data['total_annual_comp_added']):>14} "
f"{fmt(data['total_first_year_cost']):>14} "
f"{fmt(data['recruiter_fees']):>12}")
# By function
print(f"\n[ HEADCOUNT BY FUNCTION ]")
print(sep)
print(f" {'Function':<18} {'Hires':>7} {'Annual Comp':>14} {'1yr Cost':>14}")
print(f" {'-'*18} {'-'*7} {'-'*14} {'-'*14}")
for fn, data in sorted(fn_summary.items(), key=lambda x: -x[1]["count"]):
print(f" {fn:<18} {data['count']:>7} {fmt(data['total_comp']):>14} {fmt(data['total_first_year']):>14}")
# Hire detail
print(f"\n[ HIRE DETAIL ]")
print(sep)
print(f" {'Role':<30} {'Fn':<14} {'Lvl':<6} {'Q':<8} {'Base':>10} {'Total Comp':>12} {'Priority':<8}")
print(f" {'-'*30} {'-'*14} {'-'*6} {'-'*8} {'-'*10} {'-'*12} {'-'*8}")
for h in sorted(plan.hires, key=lambda x: quarter_to_sortkey(x.quarter)):
costs = compute_hire_costs(h)
print(f" {h.role:<30} {h.function:<14} {h.level:<6} {h.quarter:<8} "
f"{fmt(h.base_salary):>10} {fmt(costs['total_comp']):>12} {h.priority:<8}")
if h.business_case:
bc = h.business_case[:60] + "..." if len(h.business_case) > 60 else h.business_case
print(f" {'':>30} ↳ {bc}")
# Risk assessment
print(f"\n[ RISK ASSESSMENT ]")
print(sep)
sev_order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "INFO": 3}
for risk in sorted(risks, key=lambda r: sev_order.get(r["severity"], 99)):
sev = risk["severity"]
marker = {"HIGH": "⚠ HIGH", "MEDIUM": "◆ MED ", "LOW": "◇ LOW ", "INFO": "ℹ INFO"}[sev]
print(f"\n [{marker}] {risk['category']}")
# Wrap finding
finding = risk["finding"]
words = finding.split()
line = " Finding: "
for w in words:
if len(line) + len(w) + 1 > WIDTH - 2:
print(line)
line = " " + w + " "
else:
line += w + " "
if line.strip():
print(line)
reco = risk["recommendation"]
words = reco.split()
line = " Action: "
for w in words:
if len(line) + len(w) + 1 > WIDTH - 2:
print(line)
line = " " + w + " "
else:
line += w + " "
if line.strip():
print(line)
print(f"\n{SEP}\n")
def export_csv(plan: HiringPlan) -> str:
"""Return CSV of hire detail."""
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["Role", "Function", "Level", "Quarter", "Priority",
"Base Salary", "Bonus Target", "Equity Annual", "Benefits",
"Total Comp", "Recruiter Fee", "Overhead", "First Year Total",
"Ramp Months", "Open to Internal", "Business Case"])
for h in plan.hires:
c = compute_hire_costs(h)
writer.writerow([h.role, h.function, h.level, h.quarter, h.priority,
h.base_salary, c["target_bonus"], h.equity_annual_usd, h.benefits_annual,
c["total_comp"], c["recruiter_fee"], c["overhead"], c["first_year_total"],
h.ramp_months, h.open_to_internal, h.business_case])
return output.getvalue()
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
def build_sample_plan() -> HiringPlan:
"""Sample Series A → B hiring plan."""
plan = HiringPlan(
company="AcmeTech (Series A)",
plan_period="2025 Annual",
current_headcount=32,
current_revenue=3_500_000,
target_revenue=8_000_000,
overhead_rate=0.25,
internal_recruiter_cost=140_000,
)
plan.hires = [
# Q1 — Foundation hires
HireTarget(
role="Staff Software Engineer (Backend)",
level="L4", function="Engineering", quarter="Q1-2025",
base_salary=185_000, bonus_pct=0.0, equity_annual_usd=25_000,
benefits_annual=18_000, recruiter_fee_pct=0.0, ramp_months=2,
priority="High", open_to_internal=True,
business_case="Core API team is bottleneck for 3 roadmap items. Staff-level needed to lead architecture."
),
HireTarget(
role="Account Executive (Mid-Market)",
level="L3", function="Sales", quarter="Q1-2025",
base_salary=95_000, bonus_pct=0.50, equity_annual_usd=10_000,
benefits_annual=15_000, recruiter_fee_pct=0.18, ramp_months=4,
priority="High",
business_case="Pipeline coverage at 1.8x quota. Need 2.5x by Q2. AE adds $600K ARR/year at ramp."
),
HireTarget(
role="Product Designer (Senior)",
level="L3", function="Product", quarter="Q1-2025",
base_salary=145_000, bonus_pct=0.0, equity_annual_usd=18_000,
benefits_annual=18_000, recruiter_fee_pct=0.0, ramp_months=2,
priority="High",
business_case="Single designer for 4 squads. UX debt slowing enterprise deals requiring onboarding improvements."
),
# Q2 — Growth hires
HireTarget(
role="Engineering Manager (Frontend)",
level="M1", function="Engineering", quarter="Q2-2025",
base_salary=175_000, bonus_pct=0.10, equity_annual_usd=22_000,
benefits_annual=18_000, recruiter_fee_pct=0.20, ramp_months=3,
priority="High",
business_case="Frontend team at 7 ICs with no dedicated EM. Performance review debt is high; manager needed."
),
HireTarget(
role="Account Executive (Mid-Market)",
level="L2", function="Sales", quarter="Q2-2025",
base_salary=85_000, bonus_pct=0.50, equity_annual_usd=8_000,
benefits_annual=15_000, recruiter_fee_pct=0.18, ramp_months=4,
priority="High",
business_case="Second AE to reach 2.5x pipeline coverage target."
),
HireTarget(
role="Customer Success Manager",
level="L2", function="Customer Success", quarter="Q2-2025",
base_salary=90_000, bonus_pct=0.15, equity_annual_usd=8_000,
benefits_annual=15_000, recruiter_fee_pct=0.0, ramp_months=2,
priority="Medium",
business_case="CSM:account ratio at 1:60, industry standard 1:30. NRR has dipped 4pts in 2 quarters."
),
HireTarget(
role="Data Engineer",
level="L2", function="Engineering", quarter="Q2-2025",
base_salary=155_000, bonus_pct=0.0, equity_annual_usd=18_000,
benefits_annual=18_000, recruiter_fee_pct=0.0, ramp_months=3,
priority="Medium",
business_case="Analytics infrastructure blocking product analytics, customer dashboards, and board metrics."
),
# Q3 — Scale hires
HireTarget(
role="Senior Software Engineer (Backend)",
level="L3", function="Engineering", quarter="Q3-2025",
base_salary=165_000, bonus_pct=0.0, equity_annual_usd=20_000,
benefits_annual=18_000, recruiter_fee_pct=0.0, ramp_months=2,
priority="High",
business_case="Backend team needs capacity to deliver Q3 roadmap without delaying Q4 items."
),
HireTarget(
role="Head of Marketing",
level="M3", function="Marketing", quarter="Q3-2025",
base_salary=180_000, bonus_pct=0.15, equity_annual_usd=30_000,
benefits_annual=18_000, recruiter_fee_pct=0.20, ramp_months=3,
priority="High",
business_case="No marketing function. 100% of pipeline is outbound. Need inbound by Q1-2026 for Series B."
),
HireTarget(
role="People Operations Manager",
level="M1", function="G&A", quarter="Q3-2025",
base_salary=120_000, bonus_pct=0.10, equity_annual_usd=12_000,
benefits_annual=16_000, recruiter_fee_pct=0.0, ramp_months=2,
priority="Medium",
business_case="Founders spending 8hrs/week on HR ops at 40 employees. Unscalable. First dedicated HR hire."
),
# Q4 — Stretch hires (conditional on revenue milestone)
HireTarget(
role="Senior Software Engineer (Frontend)",
level="L3", function="Engineering", quarter="Q4-2025",
base_salary=160_000, bonus_pct=0.0, equity_annual_usd=18_000,
benefits_annual=18_000, recruiter_fee_pct=0.0, ramp_months=2,
priority="Medium",
business_case="Conditional on Q3 ARR exceeding $5.5M. Frontend team capacity planning for 2026 roadmap."
),
HireTarget(
role="Account Executive (Enterprise)",
level="L4", function="Sales", quarter="Q4-2025",
base_salary=120_000, bonus_pct=0.60, equity_annual_usd=15_000,
benefits_annual=15_000, recruiter_fee_pct=0.20, ramp_months=6,
priority="Low",
business_case="Enterprise motion exploratory. Requires ICP validation in Q2-Q3 before committing."
),
HireTarget(
role="DevOps / Platform Engineer",
level="L3", function="Engineering", quarter="Q4-2025",
base_salary=150_000, bonus_pct=0.0, equity_annual_usd=18_000,
benefits_annual=18_000, recruiter_fee_pct=0.0, ramp_months=3,
priority="Low",
business_case="Platform reliability becoming bottleneck. Conditional on uptime SLA breaches continuing in Q3."
),
]
return plan
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def load_plan_from_json(path: str) -> HiringPlan:
with open(path) as f:
data = json.load(f)
hires = [HireTarget(**h) for h in data.pop("hires", [])]
plan = HiringPlan(**data)
plan.hires = hires
return plan
def main():
parser = argparse.ArgumentParser(
description="Hiring Plan Modeler — build headcount plans with cost projections",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python hiring_plan_modeler.py # Run sample plan
python hiring_plan_modeler.py --config plan.json # Load from JSON
python hiring_plan_modeler.py --export-csv # Output CSV of hires
python hiring_plan_modeler.py --export-json # Output plan as JSON template
"""
)
parser.add_argument("--config", help="Path to JSON plan file")
parser.add_argument("--export-csv", action="store_true", help="Export hire detail as CSV")
parser.add_argument("--export-json", action="store_true", help="Export sample plan as JSON template")
args = parser.parse_args()
if args.config:
plan = load_plan_from_json(args.config)
else:
plan = build_sample_plan()
if args.export_json:
data = asdict(plan)
print(json.dumps(data, indent=2))
return
if args.export_csv:
print(export_csv(plan))
return
print_report(plan)
if __name__ == "__main__":
main()
Security leadership for growth-stage companies. Risk quantification in dollars, compliance roadmap (SOC 2/ISO 27001/HIPAA/GDPR), security architecture strate...
---
name: "ciso-advisor"
description: "Security leadership for growth-stage companies. Risk quantification in dollars, compliance roadmap (SOC 2/ISO 27001/HIPAA/GDPR), security architecture strategy, incident response leadership, and board-level security reporting. Use when building security programs, justifying security budget, selecting compliance frameworks, managing incidents, assessing vendor risk, or when user mentions CISO, security strategy, compliance roadmap, zero trust, or board security reporting."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: ciso-leadership
updated: 2026-03-05
python-tools: risk_quantifier.py, compliance_tracker.py
frameworks: risk-based-security, zero-trust, defense-in-depth
---
# CISO Advisor
Risk-based security frameworks for growth-stage companies. Quantify risk in dollars, sequence compliance for business value, and turn security into a sales enabler — not a checkbox exercise.
## Keywords
CISO, security strategy, risk quantification, ALE, SLE, ARO, security posture, compliance roadmap, SOC 2, ISO 27001, HIPAA, GDPR, zero trust, defense in depth, incident response, board security reporting, vendor assessment, security budget, cyber risk, program maturity
## Quick Start
```bash
python scripts/risk_quantifier.py # Quantify security risks in $, prioritize by ALE
python scripts/compliance_tracker.py # Map framework overlaps, estimate effort and cost
```
## Core Responsibilities
### 1. Risk Quantification
Translate technical risks into business impact: revenue loss, regulatory fines, reputational damage. Use ALE to prioritize. See `references/security_strategy.md`.
**Formula:** `ALE = SLE × ARO` (Single Loss Expectancy × Annual Rate of Occurrence). Board language: "This risk has $X expected annual loss. Mitigation costs $Y."
### 2. Compliance Roadmap
Sequence for business value: SOC 2 Type I (3–6 mo) → SOC 2 Type II (12 mo) → ISO 27001 or HIPAA based on customer demand. See `references/compliance_roadmap.md` for timelines and costs.
### 3. Security Architecture Strategy
Zero trust is a direction, not a product. Sequence: identity (IAM + MFA) → network segmentation → data classification. Defense in depth beats single-layer reliance. See `references/security_strategy.md`.
### 4. Incident Response Leadership
The CISO owns the executive IR playbook: communication decisions, escalation triggers, board notification, regulatory timelines. See `references/incident_response.md` for templates.
### 5. Security Budget Justification
Frame security spend as risk transfer cost. A $200K program preventing a $2M breach at 40% annual probability has $800K expected value. See `references/security_strategy.md`.
### 6. Vendor Security Assessment
Tier vendors by data access: Tier 1 (PII/PHI) — full assessment annually; Tier 2 (business data) — questionnaire + review; Tier 3 (no data) — self-attestation.
## Key Questions a CISO Asks
- "What's our crown jewel data, and who can access it right now?"
- "If we had a breach today, what's our regulatory notification timeline?"
- "Which compliance framework do our top 3 prospects actually require?"
- "What's our blast radius if our largest SaaS vendor is compromised?"
- "We spent $X on security last year — what specific risks did that reduce?"
## Security Metrics
| Category | Metric | Target |
|----------|--------|--------|
| Risk | ALE coverage (mitigated risk / total risk) | > 80% |
| Detection | Mean Time to Detect (MTTD) | < 24 hours |
| Response | Mean Time to Respond (MTTR) | < 4 hours |
| Compliance | Controls passing audit | > 95% |
| Hygiene | Critical patches within SLA | > 99% |
| Access | Privileged accounts reviewed quarterly | 100% |
| Vendor | Tier 1 vendors assessed annually | 100% |
| Training | Phishing simulation click rate | < 5% |
## Red Flags
- Security budget justified by "industry benchmarks" rather than risk analysis
- Certifications pursued before basic hygiene (patching, MFA, backups)
- No documented asset inventory — can't protect what you don't know you have
- IR plan exists but has never been tested (tabletop or live drill)
- Security team reports to IT, not executive level — misaligned incentives
- Single vendor for identity + endpoint + email — one breach, total exposure
- Security questionnaire backlog > 30 days — silently losing enterprise deals
## Integration with Other C-Suite Roles
| When... | CISO works with... | To... |
|---------|--------------------|-------|
| Enterprise sales | CRO | Answer questionnaires, unblock deals |
| New product features | CTO/CPO | Threat modeling, security review |
| Compliance budget | CFO | Size program against risk exposure |
| Vendor contracts | Legal/COO | Security SLAs and right-to-audit |
| M&A due diligence | CEO/CFO | Target security posture assessment |
| Incident occurs | CEO/Legal | Response coordination and disclosure |
## Detailed References
- `references/security_strategy.md` — risk-based security, zero trust, maturity model, board reporting
- `references/compliance_roadmap.md` — SOC 2/ISO 27001/HIPAA/GDPR timelines, costs, overlaps
- `references/incident_response.md` — executive IR playbook, communication templates, tabletop design
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- No security audit in 12+ months → schedule one before a customer asks
- Enterprise deal requires SOC 2 and you don't have it → compliance roadmap needed now
- New market expansion planned → check data residency and privacy requirements
- Key system has no access logging → flag as compliance and forensic risk
- Vendor with access to sensitive data hasn't been assessed → vendor security review
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Assess our security posture" | Risk register with quantified business impact (ALE) |
| "We need SOC 2" | Compliance roadmap with timeline, cost, effort, quick wins |
| "Prep for security audit" | Gap analysis against target framework with remediation plan |
| "We had an incident" | IR coordination plan + communication templates |
| "Security board section" | Risk posture summary, compliance status, incident report |
## Reasoning Technique: Risk-Based Reasoning
Evaluate every decision through probability × impact. Quantify risks in business terms (dollars, not severity labels). Prioritize by expected annual loss.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:references/compliance_roadmap.md
# Compliance Roadmap Reference
## Decision Framework: Which Framework First?
**Start here — who are your customers?**
```
Enterprise SaaS (B2B, US market) → SOC 2 Type II first
Healthcare / health data → HIPAA + SOC 2 together
EU customers or EU-resident data → GDPR (non-optional if applicable)
EU enterprise sales → ISO 27001 + GDPR
Government / defense → FedRAMP / CMMC (separate scope)
All of the above (Series B+) → Multi-framework efficiency approach
```
**The sequencing principle:** SOC 2 Type I is the fastest proof of intent (3–6 months). Type II is the credibility signal (12 months). Everything else builds on your control library.
---
## 1. SOC 2
### What It Is
SOC 2 is an attestation (not a certification) that your controls meet the AICPA Trust Service Criteria. An independent CPA firm audits your controls and issues a report.
- **Type I:** Controls are suitably designed at a point in time (snapshot). Lower credibility but faster.
- **Type II:** Controls operated effectively over a period of time (minimum 6 months). This is what enterprise buyers want.
### Trust Service Criteria (TSC)
You must include **Security** (CC). Others are optional:
| Criteria | When to add |
|---|---|
| Security (CC) | Always required |
| Availability | If uptime SLAs are contractual |
| Confidentiality | If you process confidential third-party data |
| Processing Integrity | If accuracy of processing is critical (fintech, data processing) |
| Privacy | If you make privacy commitments beyond GDPR/CCPA scope |
Most startups: **Security + Availability** is sufficient.
### Timeline: SOC 2 Type I
| Phase | Duration | Activities |
|---|---|---|
| Readiness assessment | 2–4 weeks | Gap analysis against CC criteria, identify control owners |
| Policy documentation | 4–6 weeks | Write ~15–20 policies (acceptable use, access control, change management, etc.) |
| Control implementation | 4–8 weeks | Deploy technical controls, fix gaps identified in readiness |
| Evidence collection | 2–4 weeks | Screenshots, logs, configs — auditor will sample these |
| Audit fieldwork | 2–4 weeks | CPA firm reviews evidence, interviews control owners |
| Report issuance | 2–4 weeks | Report issued, reviewed, shared with customers |
| **Total** | **3–6 months** | — |
### Timeline: SOC 2 Type II (after Type I)
| Phase | Duration | Notes |
|---|---|---|
| Observation period | 6–12 months | Controls must operate consistently — no exceptions |
| Audit fieldwork | 4–6 weeks | Auditor samples evidence across full period |
| Report issuance | 2–4 weeks | — |
| **Total from Type I** | **9–18 months** | Faster if Type I was clean |
### Cost Estimates
| Item | SOC 2 Type I | SOC 2 Type II |
|---|---|---|
| Audit firm fees | $15,000–$35,000 | $25,000–$60,000 |
| Compliance platform (Vanta, Drata, Secureframe) | $12,000–$30,000/yr | Same platform |
| External counsel / vCISO | $10,000–$30,000 | $5,000–$15,000 maintenance |
| Internal time (eng + ops) | 200–400 hours | 100–200 hours/yr |
| **Total first year** | **$40,000–$100,000** | **+$30,000–$75,000** |
**Cost optimization tips:**
- Use a compliance platform (Vanta, Drata, Secureframe) — automated evidence collection halves audit cost
- Choose a mid-tier audit firm; Big 4 is overkill for startups
- Type I and Type II with same auditor = continuity discount
### Common Failure Modes
1. Controls documented but not operating (access reviews on paper only)
2. Exceptions during observation period (one admin account without MFA = finding)
3. No formal security awareness training (required for CC criteria)
4. Change management not followed (no ticket for that production change)
5. Vendor risk management missing (you must assess your critical vendors)
---
## 2. ISO 27001
### What It Is
ISO 27001 is an internationally recognized certification for an Information Security Management System (ISMS). Unlike SOC 2, it's a certification (pass/fail), not an attestation report. Issued by accredited certification bodies (BSI, Bureau Veritas, DNV, TÜV).
**Why ISO 27001 over SOC 2:** EU enterprise buyers, government contracts, and global markets often prefer or require ISO 27001. It's geographically neutral.
### Scope Decision
ISO 27001 scope is flexible — you can certify a subset of the organization.
- **Narrow scope:** The production environment only — fastest, cheapest
- **Full scope:** Entire organization — most credibility, highest effort
- **Recommended for startups:** Production environment + key business processes
### Certification Timeline
| Phase | Duration | Activities |
|---|---|---|
| Gap analysis | 2–4 weeks | Assess current state vs. 93 controls in Annex A |
| ISMS design | 4–8 weeks | Scope, risk methodology, SoA (Statement of Applicability) |
| Policy and procedure development | 6–10 weeks | Mandatory documents: risk treatment plan, asset register, ISMS policy |
| Risk assessment | 4–6 weeks | Identify, analyze, evaluate risks; produce risk register |
| Control implementation | 8–16 weeks | Implement gaps from risk assessment |
| Internal audit | 2–4 weeks | First internal audit of ISMS |
| Management review | 1–2 weeks | Leadership sign-off on ISMS |
| Stage 1 audit (documentation) | 1–2 weeks | Certification body reviews docs and scope |
| Stage 2 audit (implementation) | 1–2 weeks | Certification body verifies controls are operating |
| Certification issued | 1–2 weeks | Certificate valid for 3 years with annual surveillance audits |
| **Total** | **9–18 months** | — |
### Cost Estimates
| Item | Cost |
|---|---|
| Certification body fees (Stage 1 + Stage 2) | $15,000–$40,000 |
| Annual surveillance audits | $8,000–$20,000/yr |
| vCISO / consultant (if not in-house) | $30,000–$80,000 |
| GRC platform | $10,000–$25,000/yr |
| Internal time | 400–800 hours |
| **Total first year** | **$55,000–$150,000** |
### Mandatory ISO 27001:2022 Documents
- ISMS scope document
- Information security policy
- Risk assessment methodology
- Risk register with risk treatment plan
- Statement of Applicability (SoA)
- Asset inventory
- Competence and awareness records
- Internal audit reports
- Management review minutes
- Nonconformity and corrective action records
---
## 3. HIPAA for Health Tech Startups
### When HIPAA Applies
HIPAA applies if you are a **Covered Entity** (healthcare provider, health plan, clearinghouse) or a **Business Associate** (you process, store, or transmit Protected Health Information on behalf of a Covered Entity).
**Key trigger:** If your product touches patient data in any way and a US healthcare provider uses your product, you are likely a Business Associate. You must sign a **BAA (Business Associate Agreement)** with each Covered Entity customer.
### HIPAA Rule Structure
| Rule | Focus | Key Requirements |
|---|---|---|
| Privacy Rule | How PHI can be used and disclosed | Minimum necessary, patient rights, notice of privacy practices |
| Security Rule | Technical and physical safeguards for ePHI | Required and addressable safeguards |
| Breach Notification Rule | What to do if PHI is breached | Timing and content of breach notifications |
### Security Rule: Required vs. Addressable
**Required safeguards** must be implemented exactly as specified. **Addressable safeguards** must be implemented or documented why an equivalent measure was used.
**Key Required Safeguards:**
- Unique user IDs (no shared logins)
- Emergency access procedure
- Audit controls (logging access to ePHI)
- Transmission security (encryption in transit)
- Person or entity authentication
**Key Addressable Safeguards (implement or document why not):**
- Automatic logoff
- Encryption and decryption (encryption at rest — despite being "addressable," regulators expect it)
- Audit review procedures
- Security reminders and training
### HIPAA Compliance Timeline
| Phase | Duration | Activities |
|---|---|---|
| Risk analysis | 4–6 weeks | Document all PHI flows, assess risks to PHI — **required by law** |
| Policy development | 4–8 weeks | Privacy policies, breach notification, workforce training |
| Technical safeguard implementation | 4–12 weeks | Encryption, audit logging, access controls, BAA templates |
| Workforce training | 2–4 weeks | Annual HIPAA training for all staff with PHI access |
| BAA execution | Ongoing | Execute with all vendors who process PHI |
| **Total** | **4–8 months** | — |
### Cost Estimates
| Item | Cost |
|---|---|
| Initial risk analysis (consultant) | $15,000–$40,000 |
| Policy development | $8,000–$20,000 |
| Technical implementation | $20,000–$60,000 |
| Annual training and maintenance | $5,000–$15,000/yr |
| HIPAA compliance platform | $10,000–$20,000/yr |
| **Total first year** | **$45,000–$130,000** |
### HIPAA Penalties (Why This Matters)
| Violation Category | Penalty per Violation | Annual Cap |
|---|---|---|
| Unaware | $100–$50,000 | $25,000 |
| Reasonable cause | $1,000–$50,000 | $100,000 |
| Willful neglect (corrected) | $10,000–$50,000 | $250,000 |
| Willful neglect (not corrected) | $50,000 | $1,500,000 |
---
## 4. GDPR Compliance Program
### When GDPR Applies
GDPR applies if you:
- Are established in the EU/EEA
- Process personal data of EU/EEA residents (regardless of your location)
- Offer goods or services to EU residents
- Monitor the behavior of EU residents
**Key point for US startups:** If you have EU users or EU employees, GDPR applies to you.
### Core GDPR Principles (Build These In)
1. **Lawfulness, fairness, transparency** — have a legal basis for every processing activity
2. **Purpose limitation** — collect data for specified, explicit purposes only
3. **Data minimization** — collect only what you need
4. **Accuracy** — keep data accurate
5. **Storage limitation** — delete data when no longer needed
6. **Integrity and confidentiality** — appropriate security measures
7. **Accountability** — demonstrate compliance
### Legal Bases for Processing
| Basis | When to use |
|---|---|
| Consent | Marketing, non-essential cookies, optional features |
| Contract | Processing necessary to deliver your service |
| Legitimate interests | Analytics, fraud prevention, security (requires LIA) |
| Legal obligation | Compliance with legal requirements |
| Vital interests | Emergency situations only |
**Avoid over-relying on consent** — it must be freely given, specific, informed, and unambiguous. Contractual basis is more robust for core product data.
### GDPR Compliance Checklist
**Governance:**
- [ ] Data Protection Officer (DPO) appointed (required for large-scale processing or sensitive data)
- [ ] Record of Processing Activities (RoPA) maintained
- [ ] Data Protection Impact Assessments (DPIA) for high-risk processing
**Rights Management (respond within 1 month):**
- [ ] Right of access (data subject access requests — DSARs)
- [ ] Right to rectification
- [ ] Right to erasure ("right to be forgotten")
- [ ] Right to data portability
- [ ] Right to object to processing
**Technical Measures:**
- [ ] Privacy by design in product development
- [ ] Data minimization enforced
- [ ] Encryption at rest and in transit
- [ ] Pseudonymization where possible
- [ ] Retention policies and automated deletion
**Vendor Management:**
- [ ] Data Processing Agreements (DPAs) with all processors
- [ ] Standard Contractual Clauses (SCCs) for non-EU transfers
**Breach Notification:**
- [ ] Notify supervisory authority within 72 hours of awareness
- [ ] Notify affected individuals if high risk to their rights and freedoms
### GDPR Compliance Timeline
| Phase | Duration | Activities |
|---|---|---|
| Data mapping | 3–6 weeks | Map all personal data flows: collect, store, process, share, delete |
| Legal basis review | 2–4 weeks | Assign legal basis to each processing activity |
| Policy updates | 4–6 weeks | Privacy policy, cookie policy, employee data notices |
| DPA execution | 2–4 weeks | Execute DPAs with all processors (SaaS vendors, cloud providers) |
| Technical controls | 4–12 weeks | Consent management, data subject rights automation, retention |
| Staff training | 2–4 weeks | GDPR awareness for all staff |
| **Total** | **3–6 months** | — |
### GDPR Fines
- **Standard violations:** Up to €10M or 2% of global annual revenue
- **Major violations** (basic principles, consent, data subject rights): Up to €20M or 4% of global annual revenue
- **Highest ever fine:** Meta, €1.2B (2023, data transfers to US)
---
## 5. Multi-Framework Efficiency
### Control Overlap Analysis
The same underlying controls satisfy multiple frameworks. Build once, certify multiple times.
**Core Control Domain Overlap:**
| Control Domain | SOC 2 | ISO 27001 | HIPAA | GDPR |
|---|---|---|---|---|
| Access control / IAM | CC6 | A.5.15–A.5.18 | §164.312(a) | Art. 32 |
| Encryption at rest/transit | CC6.7 | A.8.24 | §164.312(a)(2)(iv) | Art. 32 |
| Audit logging | CC7.2 | A.8.15, A.8.17 | §164.312(b) | Art. 32 |
| Incident response | CC7.3–CC7.5 | A.5.24–A.5.28 | §164.308(a)(6) | Art. 33–34 |
| Vendor/third-party mgmt | CC9 | A.5.19–A.5.22 | §164.308(b) | Art. 28 |
| Risk assessment | CC3 | Clause 6.1 | §164.308(a)(1) | Art. 32 |
| Security training | CC1.4 | A.6.3, A.6.8 | §164.308(a)(5) | Art. 39 |
| Business continuity | A1 | A.5.29–A.5.30 | §164.308(a)(7) | Art. 32 |
| Data classification | CC6.1 | A.5.9–A.5.13 | §164.514 | Art. 5(1)(c) |
| Change management | CC8 | A.8.32 | §164.312(c) | Art. 25 |
**Efficiency Rule:** If you build SOC 2 controls correctly, you're ~65–75% of the way to ISO 27001 and ~70% of the way to HIPAA. Don't rebuild — extend.
### Recommended Sequencing by Company Profile
**B2B SaaS (US-focused):**
```
Month 0–6: SOC 2 Type I → unblocks early enterprise deals
Month 6–18: SOC 2 Type II → enterprise table stakes
Month 18–30: ISO 27001 → EU market expansion
(GDPR should be woven in from month 0 if any EU data)
```
**HealthTech (US):**
```
Month 0–8: HIPAA compliance + BAA readiness → enables healthcare customers
Month 6–18: SOC 2 Type II → enterprise IT requirements on top of HIPAA
Month 18+: ISO 27001 if entering European market
```
**EU-founded SaaS:**
```
Month 0–3: GDPR compliance → legal requirement, not optional
Month 3–12: ISO 27001 → EU enterprise default expectation
Month 12–24: SOC 2 → US market expansion
```
**HealthTech (EU):**
```
Concurrent: GDPR + ISO 27001 (strong overlap with MDR/IVDR security requirements)
Month 12+: HIPAA if entering US market
```
### Shared Evidence Model
Build your evidence library once. Tag each piece of evidence by framework:
```
evidence/
├── access_control/
│ ├── iam_policy.pdf [SOC2:CC6, ISO:A5.15, HIPAA:164.312a]
│ ├── mfa_screenshot_Q1.png [SOC2:CC6, ISO:A8.5, HIPAA:164.312d]
│ └── access_review_log.xlsx [SOC2:CC6, ISO:A5.18, HIPAA:164.308a]
├── encryption/
│ ├── kms_config.png [SOC2:CC6.7, ISO:A8.24, HIPAA:164.312e]
│ └── tls_policy.md [SOC2:CC6.7, ISO:A8.24, HIPAA:164.312e]
└── incident_response/
├── ir_plan.pdf [SOC2:CC7, ISO:A5.24, HIPAA:164.308a6]
└── tabletop_log.pdf [SOC2:CC7, ISO:A5.26, HIPAA:164.308a6]
```
### GRC Platform Comparison
| Platform | Best For | Price/yr | SOC 2 | ISO 27001 | HIPAA | GDPR |
|---|---|---|---|---|---|---|
| Vanta | Fast SOC 2, US startups | $15–30K | ✅ | ✅ | ✅ | ✅ |
| Drata | Automation depth | $18–35K | ✅ | ✅ | ✅ | ✅ |
| Secureframe | Cost-effective | $10–20K | ✅ | ✅ | ✅ | ✅ |
| Sprinto | SMB, global | $12–25K | ✅ | ✅ | ✅ | ✅ |
| Tugboat Logic | Mid-market | $20–40K | ✅ | ✅ | ✅ | ✅ |
| Manual | Budget-constrained | $0 + time | ✅ | ✅ | ✅ | ✅ |
**Recommendation:** For Series A startups, Vanta or Drata pays for itself in reduced auditor fees and internal time savings. Budget $15–25K/year.
### Compliance Maintenance Annual Budget
| Item | SOC 2 | ISO 27001 | HIPAA | GDPR |
|---|---|---|---|---|
| Annual audit / surveillance | $25–60K | $8–20K | n/a (self-assessed) | n/a (self-assessed) |
| GRC platform | $15–30K | Shared | Shared | Shared |
| Annual training | $3–8K | Shared | Shared | Shared |
| Policy review | $2–5K | $2–5K | $2–5K | $2–5K |
| **Total ongoing** | **$45–103K/yr** | **+$10–25K/yr** | **+$5–15K/yr** | **+$5–15K/yr** |
FILE:references/incident_response.md
# Incident Response Reference (Executive Playbook)
This is the executive IR playbook — strategic decisions, communication, and leadership during incidents. For technical playbooks (containment procedures, forensics), see your SOC runbooks.
---
## 1. Incident Classification
### Severity Levels
| Severity | Definition | Examples | Response Time | Escalation |
|---|---|---|---|---|
| SEV-1 (Critical) | Confirmed breach, data exfil, ransomware, production down | Active ransomware, confirmed data theft, complete service outage | Immediate (< 1 hour) | CEO, board within 24 hrs |
| SEV-2 (High) | Suspected breach, significant security event, extended outage | Credential compromise suspected, DDoS, 4-hour+ outage | < 4 hours | CEO, legal within 48 hrs |
| SEV-3 (Medium) | Security event with limited impact, short outage | Phishing success (contained), brief outage, single system compromise | < 24 hours | CISO-owned, weekly rollup |
| SEV-4 (Low) | Minor security event, near-miss | Failed phishing attempt, minor policy violation | < 72 hours | Team-owned |
### Breach vs. Security Incident
**Security incident:** Unplanned event affecting security — may or may not involve data.
**Data breach:** Confirmed unauthorized access to personal data — triggers regulatory notification obligations.
**Critical distinction for response planning:** A ransomware attack is an incident. If data was exfiltrated before encryption, it's also a breach. Assume breach until proven otherwise.
---
## 2. Executive IR Plan
### Phase 1: Detection & Initial Assessment (0–2 hours for SEV-1)
**Immediate actions (CISO):**
1. Receive alert from SOC/monitoring system or team member report
2. Make initial severity classification — don't wait for perfect information
3. Activate incident response team (IR lead, legal counsel, comms lead)
4. Create incident war room (dedicated Slack channel, video bridge, shared document)
5. **Stop the clock** — document exact time of discovery (regulatory timelines start here)
6. Begin chain of custody documentation if forensics may be needed
**Executive notification trigger (within 1 hour for SEV-1):**
- Notify CEO: incident status, initial severity, IR team activated
- Put legal counsel on notice — don't wait to determine if breach occurred
- If public company: notify General Counsel immediately (potential disclosure obligations)
**What you do NOT do in Phase 1:**
- Do not notify customers yet (confirm scope first)
- Do not delete or modify any logs or systems (evidence preservation)
- Do not make public statements
- Do not speculate about cause or scope
### Phase 2: Containment & Assessment (2–24 hours for SEV-1)
**Executive decisions required:**
- **Scope authorization:** Approve IR firm engagement (have a retainer in place)
- **System isolation:** Authorize taking systems offline if needed (revenue vs. evidence tradeoff)
- **Evidence preservation:** Authorize forensic image capture
- **Communication timing:** When to notify customers/partners (legal drives this)
**Board notification (for SEV-1/2):**
- Notify board chair / audit committee chair within 24 hours for SEV-1
- Board notification format: what we know, what we don't know, what we're doing, next update time
- Do not speculate on financial impact in board notification until known
**Legal assessment (with counsel):**
- Determine if personal data was involved
- Identify applicable notification laws (GDPR 72-hour, state breach notification, HIPAA 60-day)
- Assess litigation risk (document with privilege from this point)
- Evaluate cyber insurance policy coverage and notification requirements
### Phase 3: Notification & Communication (24–72 hours for SEV-1)
**Notification decision matrix:**
| Audience | Trigger | Timeline | Owner |
|---|---|---|---|
| Board | SEV-1/2 confirmed | < 24 hours | CEO/CISO |
| Regulators (GDPR) | Personal data breach confirmed | < 72 hours from awareness | Legal + CISO |
| Regulators (HIPAA) | PHI breach confirmed | < 60 days (early notice to HHS ASAP) | Legal + CISO |
| State regulators (US) | State breach notification laws vary | 30–90 days depending on state | Legal |
| Enterprise customers | Data confirmed in scope | As soon as practical after legal review | CEO/CRO |
| All customers | Data potentially in scope | After regulators notified | CEO/Comms |
| Media | Proactive or reactive | After notifying affected parties | CEO/Comms |
| Cyber insurer | Incident confirmed | Per policy terms (often 48–72 hours) | CFO/Legal |
### Phase 4: Recovery (Ongoing)
**Executive decisions:**
- Approve recovery timeline and communicate to customers
- Determine customer compensation or remediation (if applicable)
- Authorize security improvements identified during incident
- Decide on public disclosure beyond mandatory reporting
### Phase 5: Post-Incident Review (Within 30 days)
Covered in Section 5 of this document.
---
## 3. Communication Templates
### Board/Executive Notification (Initial — Hour 1)
**Subject:** [CONFIDENTIAL] Security Incident — Immediate Notification
---
We have identified a security incident as of [DATE/TIME].
**Current status:** [Brief factual description — what we know happened]
**Severity assessment:** SEV-[1/2/3]
**What we do not yet know:**
- [List unknowns — scope of impact, whether data was accessed, root cause]
**Actions taken so far:**
- IR team activated at [time]
- Legal counsel notified
- [Specific containment actions if applicable]
**Next update:** [Specific time, e.g., "in 4 hours or when we have material new information"]
**Who is managing this:** [CISO name] leads technical response; [CEO name] owns executive decisions. Contact: [CISO mobile]
---
### Customer Notification (After Legal Review)
**Subject:** Important Security Notice — [Company Name]
---
We are writing to inform you of a security incident that may have affected your data.
**What happened:**
On [DATE], we detected [brief, factual description of the incident — e.g., "unauthorized access to our systems"]. We identified this on [DISCOVERY DATE] and immediately launched an investigation.
**What information was involved:**
Based on our investigation, the following types of information may have been accessed: [list data types — e.g., names, email addresses, [if applicable: payment card information]].
Your [specific data types] [were / were not] affected.
**What we are doing:**
We have [list specific actions: engaged leading cybersecurity firm, notified relevant authorities, implemented additional security controls, etc.].
**What you can do:**
- [Specific actionable steps for customers]
- Monitor your accounts for unusual activity
- [If passwords: reset your password at X]
- [If payment data: contact your bank to monitor for unauthorized charges]
- Contact our dedicated support line at [contact] with any concerns
**For more information:**
We have set up a dedicated resource page at [URL]. Our support team is available at [contact].
We take the security of your data extremely seriously and deeply regret this incident occurred.
[CEO/CISO Name]
[Title], [Company Name]
---
### Regulator Notification — GDPR (72-hour requirement)
**To:** [Relevant Supervisory Authority — e.g., BfDI (Germany), CNIL (France), ICO (UK)]
**Subject:** Personal Data Breach Notification — [Company Name] — [Reference Number if applicable]
---
**1. Nature of the breach:**
[Description of what occurred, including how it happened]
**2. Categories and approximate number of data subjects concerned:**
[e.g., "Approximately [X] customers whose [name, email, account data] may have been accessed"]
**3. Categories and approximate number of personal data records concerned:**
[e.g., "Approximately [X] records containing [data categories]"]
**4. Likely consequences of the breach:**
[Risk assessment: what harm could data subjects face?]
**5. Measures taken or proposed:**
[Containment actions, remediation plan, customer notification plan]
**6. Contact details of the Data Protection Officer or other contact point:**
[Name, role, email, phone]
**Note:** This is an initial notification; we will provide supplemental information as our investigation continues.
---
### Media Statement (Reactive — When Contacted)
"[Company Name] is aware of a security incident that we identified on [date]. We immediately activated our incident response team and launched a comprehensive investigation. We have notified affected customers and relevant regulatory authorities as required. The security and privacy of our customers' data is our top priority, and we are committed to transparency as our investigation proceeds. We will provide updates at [URL]. We cannot provide additional details at this time to protect the integrity of our investigation."
**What not to say to media:**
- Number of affected users (until confirmed and disclosed to customers first)
- Cause of the incident (until investigation is complete)
- Financial impact (speculation creates liability)
- Anything that could be construed as minimizing the incident
---
## 4. Tabletop Exercise Design
### Purpose
Test the decision-making and communication processes — not the technical response. The goal is to surface gaps in escalation, communication, and judgment before a real incident.
### Recommended Frequency
- Annual full tabletop (2–3 hours, full leadership team)
- Semi-annual mini-tabletop (45 minutes, CISO + legal + CEO)
- Quarterly technical team exercise (separate from executive tabletop)
### Sample Tabletop Scenario: Ransomware
**Setup (read to participants):**
> It's 6:47 AM on a Monday. Your DevOps engineer receives automated alerts that production databases are inaccessible. By 7:15 AM, they discover a ransomware note demanding $500,000 in Bitcoin. Several files are already encrypted. Your last verified backup was 48 hours ago. Your business is B2B SaaS serving 200 enterprise customers. You process customer financial data.
**Discussion questions (timed, 10 minutes each):**
1. First 30 minutes — who do you call, in what order? Who decides whether to take production offline?
2. Legal assessment — what regulatory obligations have been triggered? What's the timeline?
3. Hour 4 — initial forensics suggests data may have been exfiltrated before encryption. How does your response change?
4. Customer communication — how do you communicate with enterprise customers who are asking for status?
5. Hour 24 — do you pay the ransom? Who makes this decision? What's the decision framework?
6. The press has found out and a reporter is calling. What do you say?
7. Day 5 — what's your board communication strategy?
**Post-discussion captures:**
- What decisions were unclear (ownership ambiguous)?
- What information did you need but didn't have?
- What processes did not exist that should?
- What would you do differently in the first hour?
### Sample Tabletop Scenario: Insider Threat
**Setup:**
> HR notifies you that an engineer was terminated this morning for performance reasons. 24 hours later, your SIEM generates an alert that this former employee's credentials accessed your customer database 30 minutes before their offboarding was complete. They downloaded 50,000 customer records. You don't know if they shared or sold the data.
**Key decision points:**
- When does this become a breach vs. a security incident?
- Do you notify customers? When?
- What are your legal options against the former employee?
- How do you handle this with the rest of the engineering team?
---
## 5. Post-Incident Review Framework
### Timeline
Conduct within 30 days of incident resolution. Do not delay — memory fades and teams move on.
### Blameless Post-Mortem Principles
The purpose is to improve systems and processes, not punish individuals. A blame culture means the next incident gets hidden longer.
### Post-Incident Review Structure
**1. Incident Timeline (factual, no editorializing)**
- Hour-by-hour reconstruction from detection to resolution
- Source: logs, Slack messages, incident ticket, war room notes
**2. Root Cause Analysis**
Use the "5 Whys" technique — keep asking why until you reach a systemic root cause, not a human error.
Example:
- Why was there a breach? → Attacker compromised an admin account
- Why was the admin account compromised? → Credentials stolen via phishing
- Why did phishing succeed? → User wasn't trained on this attack type
- Why wasn't training current? → Training program hadn't been updated in 18 months
- Why hadn't it been updated? → No owner was assigned to maintain the training program
- **Root cause: No assigned ownership for security training maintenance**
**3. What Went Well**
- Detection mechanisms that worked
- Response actions that contained damage
- Communication that was effective
- Teams that exceeded expectations
**4. What Needs Improvement**
- Detection gaps (how could we have found this faster?)
- Response gaps (what slowed us down?)
- Communication gaps (who didn't know what, when?)
- Process gaps (what didn't we have documented?)
**5. Action Items (with owners and deadlines)**
| Action | Owner | Due Date | Priority |
|---|---|---|---|
| [Specific improvement] | [Name] | [Date] | [P0/P1/P2] |
**6. Metrics Review**
- MTTD (Mean Time to Detect): [actual] vs. [target]
- MTTR (Mean Time to Respond): [actual] vs. [target]
- Customer impact: [affected customers, duration]
- Financial impact: [direct costs, revenue impact]
- Regulatory impact: [notifications sent, fines if any]
---
## 6. Insurance and Legal Considerations
### Cyber Insurance
**What to have before an incident:**
- Cyber liability policy with minimum $2M coverage (Series A); $5M+ (Series B+)
- Coverage should include: first-party loss, third-party liability, ransomware, business interruption, regulatory defense
- Pre-approved IR firms on your policy (using an approved firm can expedite claims)
- Notification requirements — know your insurer's required timeline (typically 48–72 hours)
**Policy exclusions to watch:**
- "War exclusion" — increasingly contested for nation-state attacks (NotPetya precedent)
- "Systemic risk" — some policies exclude widespread events affecting many insureds simultaneously
- "Prior acts" — incidents that began before policy inception
- "Failure to maintain reasonable security" — don't give your insurer a reason to deny
**Premium factors:**
- Revenue and data volume
- Security control maturity (MFA, EDR, backup, patch management)
- Industry (healthcare, financial services = higher premium)
- Claims history
**Ballpark premiums:**
- Seed/Series A ($1–10M ARR): $8,000–$25,000/yr
- Series B ($10–50M ARR): $25,000–$75,000/yr
- Series C+ ($50M+ ARR): $75,000–$250,000/yr
### Legal Counsel
**Have on retainer before an incident:**
- Cybersecurity/privacy attorney — breach notification, regulatory response
- General counsel — contracts, employment law (insider threats), litigation
- Consider: a law firm with data breach notification experience by jurisdiction
**Attorney-client privilege:** Once legal counsel is involved in an incident, communications and work product may be privileged. Engage counsel early to maximize privilege protection.
**Key legal decisions during an incident:**
- When does notification obligation clock start? (Legal determines this)
- Is this a breach or an incident? (Legal + CISO together)
- Who are the affected data subjects? (Legal + technical together)
- Do we pay the ransom? (Legal, CEO, board — never CISO alone)
- Do we cooperate with law enforcement? (Legal decision, involves trade-offs)
### Law Enforcement
**FBI Internet Crime Complaint Center (IC3):** File a complaint for ransomware or significant cybercrime. Does not obligate you to cooperate but creates a record.
**Pros of law enforcement involvement:**
- Access to threat intelligence they may have
- May recover funds in some cases (rare)
- Demonstrates good-faith response to regulators
**Cons of law enforcement involvement:**
- Loss of control over investigation timeline
- Potential for public disclosure if case pursued
- Slows ransom payment decisions (if considering)
- May create discovery obligations in litigation
**CISO recommendation:** Notify legal before contacting law enforcement. In most cases, file an IC3 complaint but don't actively engage FBI investigation unless there's a clear benefit.
FILE:references/security_strategy.md
# Security Strategy Reference
## 1. Risk-Based Security (Not Compliance-First)
### The Problem with Compliance-First Security
Most startups build security backwards: they get a compliance requirement (SOC 2, ISO 27001) and treat it as the security program. This produces:
- Controls that pass audits but don't reduce actual risk
- Resources allocated to documentation over protection
- Security teams optimizing for auditor satisfaction, not threat reduction
- False confidence ("we passed our audit") before real security exists
**The right order:**
1. Identify your actual threats (what do adversaries want from you?)
2. Identify your crown jewels (what's worth protecting most?)
3. Implement controls that address those threats to those assets
4. Map existing controls to compliance requirements — most overlap naturally
### Risk Identification Framework
**Asset Classification:**
```
Tier 1 — Crown Jewels
├── Customer PII/PHI
├── Payment card data
├── Intellectual property (source code, models, trade secrets)
└── Authentication credentials and secrets
Tier 2 — Business Critical
├── Internal communications (Slack, email)
├── Financial systems and data
├── Employee data
└── Business strategy documents
Tier 3 — Operational
├── Internal tooling and infrastructure configs
├── Non-sensitive operational data
└── Public-facing content and marketing
```
**Threat Actor Profiling:**
| Threat Actor | Motivation | Typical TTPs | Relative Likelihood |
|---|---|---|---|
| Financially motivated criminals | Data theft, ransomware | Phishing, credential stuffing | High |
| Nation-state | IP theft, espionage | Spear phishing, supply chain | Low-Medium (sector-dependent) |
| Insider threat | Financial gain, revenge | Privilege abuse, data exfil | Medium |
| Script kiddies | Notoriety, fun | Known CVEs, scanning | High (low sophistication) |
| Competitors | IP theft | Social engineering, insider recruitment | Low-Medium |
### Risk Quantification (FAIR Model Simplified)
**Annual Loss Expectancy:**
```
ALE = SLE × ARO
SLE (Single Loss Expectancy) = Asset Value × Exposure Factor
ARO (Annual Rate of Occurrence) = historical frequency or industry estimate
```
**Business Impact Categories:**
- **Direct financial loss**: fraud, ransomware payment, theft
- **Regulatory fines**: GDPR (4% global revenue), HIPAA ($100–$50K per violation), PCI DSS
- **Revenue impact**: customer churn post-breach, deal loss during incident, downtime cost
- **Reputational damage**: brand devaluation (harder to quantify, but real)
- **Legal costs**: incident response counsel, class action defense, settlements
**Example Risk Quantification:**
| Risk Scenario | SLE | ARO | ALE |
|---|---|---|---|
| Customer data breach (10K records) | $850K | 0.15 | $127,500/yr |
| Ransomware attack | $350K | 0.20 | $70,000/yr |
| Credential compromise + fraud | $120K | 0.35 | $42,000/yr |
| Third-party SaaS breach | $95K | 0.25 | $23,750/yr |
| Insider data exfiltration | $180K | 0.10 | $18,000/yr |
**Mitigation ROI:**
```
ROSI = (Risk Reduction × ALE) - Control Cost
────────────────────────────────────
Control Cost
Example: MFA deployment
Risk reduction: 99% for credential attacks
ALE reduced: $42,000 × 0.99 = $41,580
Control cost: $5,000/yr
ROSI: ($41,580 - $5,000) / $5,000 = 731%
```
---
## 2. Zero Trust Architecture at Strategy Level
### What Zero Trust Actually Means
Zero trust is not a product — it's an architectural principle: **never trust, always verify, assume breach.**
The traditional perimeter model (trust inside the network, distrust outside) fails because:
- Remote work destroyed the perimeter
- Cloud infrastructure has no perimeter
- 80% of breaches involve privileged account abuse (internal trust abused)
- Supply chain attacks compromise trusted software
### Zero Trust Maturity Model
**Stage 1 — Identity-Centric (Start Here)**
- MFA enforced for all users, all applications
- Identity provider (Okta, Azure AD, Google Workspace) as single control plane
- No shared service accounts
- Privileged Access Management (PAM) for admin access
- **Cost:** $20–80K/year | **Timeline:** 3–6 months
**Stage 2 — Device Trust**
- Endpoint detection and response (EDR) on all devices
- Device health checks before granting access
- Mobile device management (MDM) for BYOD
- Certificate-based device authentication
- **Cost:** $30–60K/year additional | **Timeline:** 6–12 months
**Stage 3 — Network Micro-Segmentation**
- Replace VPN with Zero Trust Network Access (ZTNA)
- Segment production from development from corporate
- East-west traffic inspection (not just north-south)
- **Cost:** $40–100K/year additional | **Timeline:** 12–18 months
**Stage 4 — Application-Level Controls**
- Just-in-time access (no standing privileges)
- Workload identity for service-to-service auth
- API gateway with authentication enforcement
- Continuous authorization (not just at login)
- **Cost:** $50–150K/year additional | **Timeline:** 18–30 months
**Strategic Guidance:**
- Don't sell zero trust as a project. It's a 3–5 year direction.
- Start with identity. It gives the most risk reduction per dollar.
- Measure progress by % of access covered by MFA, % of apps behind IdP, privilege account count.
---
## 3. Defense in Depth for Startups
### The Layered Security Model
```
Layer 1: Governance & Policies
└── Asset inventory, acceptable use, vendor management
Layer 2: Perimeter Controls
└── WAF, DDoS protection, email security (DMARC/DKIM/SPF)
Layer 3: Identity & Access
└── MFA, SSO, PAM, just-in-time access, least privilege
Layer 4: Endpoint Security
└── EDR, device management, patch management
Layer 5: Application Security
└── SAST/DAST, dependency scanning, code review, API security
Layer 6: Data Protection
└── Encryption at rest and in transit, DLP, backup/recovery
Layer 7: Detection & Response
└── SIEM/SOAR, log aggregation, alerting, incident response
Layer 8: Recovery
└── Backup testing, DR plan, RTO/RPO targets
```
### Startup Security Budget Allocation (Guidance)
| Stage | Annual Revenue | Recommended Security Budget | Priority Spend |
|---|---|---|---|
| Pre-seed/Seed | <$1M | 3–5% opex or $50–100K | MFA, backups, basic EDR |
| Series A | $1–10M | 2–4% revenue | +SIEM, SOC 2 Type I, AppSec |
| Series B | $10–50M | 3–5% revenue | +ZTNA, Red team, dedicated CISO |
| Series C+ | $50M+ | 4–6% revenue | +SOC, threat intelligence, M&A security |
**Non-negotiables regardless of stage:**
1. MFA on everything (particularly email, cloud consoles, code repos)
2. Automated backups with tested restore (ransomware defense)
3. Secrets management (no hardcoded credentials)
4. Dependency vulnerability scanning in CI/CD
5. Incident response plan (even a 2-page doc is better than nothing)
---
## 4. Security Program Maturity Model
**Based on NIST CSF and CMMI, simplified for startup context:**
### Level 1: Initial
- No formal policies
- Reactive security (respond to incidents, not prevent them)
- No dedicated security personnel
- Basic hygiene gaps (unpatched systems, shared passwords)
- **Typical:** Pre-seed, <20 employees
### Level 2: Developing
- Written security policies (even if not fully followed)
- Dedicated security responsibility (often part-time or dual-role)
- MFA deployed, basic asset inventory
- Incident response process documented
- SOC 2 Type I achievable from here in ~6 months
- **Typical:** Series A, 20–50 employees
### Level 3: Defined
- Security integrated into SDLC
- Dedicated security lead or vCISO
- Regular vulnerability scanning and patching
- Security awareness training program
- SOC 2 Type II and ISO 27001 achievable
- **Typical:** Series B, 50–150 employees
### Level 4: Managed
- Risk-based security program with quantified risks
- Security metrics reported to board quarterly
- Threat intelligence program
- Dedicated security team (3–8 people)
- Red team / penetration testing annually
- **Typical:** Series C+, 150–500 employees
### Level 5: Optimized
- Continuous monitoring and automated response
- Proactive threat hunting
- Industry leadership on security (bug bounty, disclosure program)
- Security as competitive advantage in sales
- **Typical:** Public company or regulated enterprise
### Maturity Assessment Questions
1. Can you list all systems that process customer data right now?
2. How long would it take to detect if an admin credential was compromised?
3. When was your last backup tested with a restore?
4. Do developers run any security checks before code is deployed?
5. Does the board receive security reporting? What's in it?
Score: 0 = no/don't know, 1 = partially, 2 = yes/verified
- 0–3: Level 1–2
- 4–7: Level 2–3
- 8–10: Level 3–4
---
## 5. Board-Level Security Reporting
### What the Board Cares About
Boards are not interested in CVE counts or firewall rules. They care about:
1. **Risk posture:** Are we getting better or worse?
2. **Regulatory exposure:** What fines could we face?
3. **Incident readiness:** If we're breached, are we prepared?
4. **Competitive position:** Do customers trust us with their data?
5. **Budget adequacy:** Are we investing appropriately?
### Quarterly Board Security Report Structure
**Executive Summary (1 page max)**
- Security posture score vs. last quarter (directional trend matters more than absolute)
- Top 3 risks and their business impact in dollars
- Key accomplishments this quarter
- Investment requested (if any)
**Risk Dashboard**
```
Risk Register Summary:
├── Critical (>$500K ALE): [count] risks, [count] mitigated
├── High ($100K–$500K ALE): [count] risks, [count] mitigated
├── Medium ($10K–$100K ALE): [count] risks
└── Low (<$10K ALE): [count] risks (for awareness only)
Trend: ↑ Risk exposure vs. Q[n-1] / ↓ Risk exposure vs. Q[n-1]
```
**Compliance Status**
- Framework certifications in scope and current status
- Next audit date
- Any findings from last audit and remediation status
**Incident Summary**
- Security incidents last quarter (count and severity)
- Time to detect / time to respond (vs. targets)
- Any regulatory reporting obligations triggered
**Key Metrics (4–6 max)**
- MFA adoption rate
- Critical patch SLA compliance
- Phishing simulation click rate (trend)
- Vendor assessments completed
**Budget Summary**
- Spend vs. budget
- Headcount
- Next quarter key investments and rationale
### Common Board Questions to Prepare For
- "Have we been breached?" (Know your detection capability, not just your answer)
- "How do we compare to peers?" (Benchmarks from Verizon DBIR, industry ISACs)
- "What's the one thing we should invest in?" (Have a clear answer)
- "If we're acquired, what would security due diligence find?" (Be honest)
- "What keeps you up at night?" (Have a real answer, not a vague one)
---
## 6. Security as Revenue Enabler
### The Sales Angle
For B2B companies, security certifications directly impact revenue:
- Enterprise buyers require SOC 2 as table stakes (increasingly SOC 2 Type II)
- Government and healthcare require ISO 27001 or HIPAA
- Passing security questionnaires faster closes deals faster
- A breach costs 10–30% customer churn; security investment is churn prevention
**How to Measure:**
- Deals blocked by security questionnaire failures (track in CRM)
- Average security questionnaire turnaround time
- Customer security reviews passed vs. failed
- Revenue attributed to new compliance certifications
### The Trust Narrative
Position security certifications in marketing:
- SOC 2 Type II: "Independently audited security controls, verified annually"
- ISO 27001: "Internationally certified information security management"
- HIPAA BAA: "Healthcare data protection to regulatory standards"
These aren't just compliance — they're trust signals that compress the sales cycle.
FILE:scripts/compliance_tracker.py
#!/usr/bin/env python3
"""
CISO Compliance Tracker
========================
Tracks compliance requirements across SOC 2, ISO 27001, HIPAA, and GDPR.
Shows control overlaps, estimates effort and cost, and prioritizes by business value.
Usage:
python compliance_tracker.py # Run with sample data
python compliance_tracker.py --json # JSON output
python compliance_tracker.py --csv output.csv # Export CSV
python compliance_tracker.py --framework soc2 # Show single framework
python compliance_tracker.py --gap-analysis # Show unaddressed requirements
python compliance_tracker.py --roadmap # Show sequenced roadmap
"""
import json
import csv
import sys
import argparse
from datetime import datetime, date
from typing import Optional
# ─── Framework Definitions ───────────────────────────────────────────────────
FRAMEWORKS = {
"soc2": {
"name": "SOC 2 Type II",
"full_name": "AICPA Trust Service Criteria — Security",
"typical_timeline_months": 12,
"typical_cost_usd": 65_000, # Audit + platform
"annual_maintenance_usd": 40_000,
"business_value": "Enterprise sales unblock, US market table stakes",
"mandatory_for": ["B2B SaaS selling to enterprise US companies"],
},
"iso27001": {
"name": "ISO 27001:2022",
"full_name": "Information Security Management System",
"typical_timeline_months": 15,
"typical_cost_usd": 95_000,
"annual_maintenance_usd": 30_000,
"business_value": "EU enterprise sales, global credibility",
"mandatory_for": ["EU enterprise customers", "Government contracts"],
},
"hipaa": {
"name": "HIPAA",
"full_name": "Health Insurance Portability and Accountability Act",
"typical_timeline_months": 7,
"typical_cost_usd": 75_000,
"annual_maintenance_usd": 20_000,
"business_value": "Healthcare customer access, BAA execution",
"mandatory_for": ["Business Associates", "Companies handling PHI"],
},
"gdpr": {
"name": "GDPR",
"full_name": "General Data Protection Regulation (EU) 2016/679",
"typical_timeline_months": 5,
"typical_cost_usd": 45_000,
"annual_maintenance_usd": 15_000,
"business_value": "EU market access, legal compliance",
"mandatory_for": ["EU-based companies", "Any company with EU user data"],
},
}
# ─── Control Domain Library ──────────────────────────────────────────────────
def build_control_domain(
domain_id: str,
name: str,
description: str,
soc2_ref: Optional[str],
iso27001_ref: Optional[str],
hipaa_ref: Optional[str],
gdpr_ref: Optional[str],
effort_days: int, # Estimated implementation effort in person-days
cost_usd: int, # Estimated implementation cost (tooling + time)
implementation_notes: str,
status: str = "Not Started", # Not Started | In Progress | Implemented | Verified
owner: Optional[str] = None,
target_date: Optional[str] = None,
) -> dict:
"""Build a control domain record."""
frameworks_applicable = []
if soc2_ref:
frameworks_applicable.append("soc2")
if iso27001_ref:
frameworks_applicable.append("iso27001")
if hipaa_ref:
frameworks_applicable.append("hipaa")
if gdpr_ref:
frameworks_applicable.append("gdpr")
return {
"domain_id": domain_id,
"name": name,
"description": description,
"references": {
"soc2": soc2_ref,
"iso27001": iso27001_ref,
"hipaa": hipaa_ref,
"gdpr": gdpr_ref,
},
"frameworks_applicable": frameworks_applicable,
"framework_count": len(frameworks_applicable),
"effort_days": effort_days,
"cost_usd": cost_usd,
"implementation_notes": implementation_notes,
"status": status,
"owner": owner,
"target_date": target_date,
}
def load_control_library() -> list[dict]:
"""
Core control domains mapped across SOC 2, ISO 27001, HIPAA, and GDPR.
Each domain represents a logical grouping of controls.
"""
controls = []
controls.append(build_control_domain(
domain_id="IAM-001",
name="Identity and Access Management",
description=(
"Unique user identities, MFA enforcement, SSO, least privilege access, "
"role-based access control, access provisioning and de-provisioning workflows."
),
soc2_ref="CC6.1, CC6.2, CC6.3",
iso27001_ref="A.5.15, A.5.16, A.5.17, A.5.18",
hipaa_ref="§164.312(a)(2)(i), §164.308(a)(3)",
gdpr_ref="Art. 32(1)(b)",
effort_days=15,
cost_usd=25_000, # SSO + MFA tooling
implementation_notes=(
"Deploy IdP (Okta/Azure AD/Google Workspace). Enforce MFA on all applications. "
"Document access provisioning process. Implement quarterly access reviews."
),
status="In Progress",
owner="IT/Security",
))
controls.append(build_control_domain(
domain_id="ENC-001",
name="Encryption at Rest and in Transit",
description=(
"Encryption of sensitive data stored in databases, file systems, and backups. "
"TLS 1.2+ for all data in transit. Key management and rotation."
),
soc2_ref="CC6.7",
iso27001_ref="A.8.24",
hipaa_ref="§164.312(a)(2)(iv), §164.312(e)(2)(ii)",
gdpr_ref="Art. 32(1)(a)",
effort_days=10,
cost_usd=8_000,
implementation_notes=(
"Enable encryption at rest on all databases (RDS, S3, etc.). "
"Configure TLS on all services. Use KMS for key management. "
"Document encryption standards in a security policy."
),
status="Implemented",
owner="Engineering",
))
controls.append(build_control_domain(
domain_id="LOG-001",
name="Audit Logging and Monitoring",
description=(
"Comprehensive logging of user activity, system events, and security events. "
"Log integrity protection. SIEM or log aggregation. Alerting on anomalies."
),
soc2_ref="CC7.2, CC7.3",
iso27001_ref="A.8.15, A.8.16, A.8.17",
hipaa_ref="§164.312(b)",
gdpr_ref="Art. 32(1)(b)",
effort_days=20,
cost_usd=30_000, # SIEM tooling
implementation_notes=(
"Centralize logs from application, infrastructure, and cloud provider. "
"Define log retention (minimum 1 year). Set up alerting for authentication "
"failures, privilege escalation, data export events."
),
status="Not Started",
owner="DevOps/Security",
))
controls.append(build_control_domain(
domain_id="IR-001",
name="Incident Response",
description=(
"Documented incident response plan. Defined severity levels. Escalation procedures. "
"Communication templates. Annual tabletop exercise. Post-incident review process."
),
soc2_ref="CC7.3, CC7.4, CC7.5",
iso27001_ref="A.5.24, A.5.25, A.5.26, A.5.27, A.5.28",
hipaa_ref="§164.308(a)(6)",
gdpr_ref="Art. 33, Art. 34",
effort_days=12,
cost_usd=10_000,
implementation_notes=(
"Write IR plan covering detection, containment, eradication, recovery, communication. "
"Define breach notification timelines (GDPR: 72 hours, HIPAA: 60 days). "
"Run annual tabletop exercise. Retain IR firm on retainer."
),
status="In Progress",
owner="CISO",
))
controls.append(build_control_domain(
domain_id="VM-001",
name="Vulnerability Management and Patching",
description=(
"Regular vulnerability scanning of infrastructure and applications. "
"Defined patch SLAs by severity. Penetration testing program. "
"Dependency vulnerability scanning in CI/CD."
),
soc2_ref="CC7.1",
iso27001_ref="A.8.8",
hipaa_ref="§164.308(a)(1)(ii)(A)",
gdpr_ref="Art. 32(1)(d)",
effort_days=15,
cost_usd=20_000,
implementation_notes=(
"Deploy infrastructure scanner (Tenable, Qualys, AWS Inspector). "
"Add SAST/DAST to CI/CD pipeline. Define patch SLAs: Critical <24h, High <7d, "
"Medium <30d. Conduct annual pentest."
),
status="In Progress",
owner="DevOps/Security",
))
controls.append(build_control_domain(
domain_id="VRISK-001",
name="Vendor and Third-Party Risk Management",
description=(
"Inventory of all third-party vendors with data access. Tiered risk assessment "
"process. Contractual security requirements. Annual reviews for critical vendors."
),
soc2_ref="CC9.2",
iso27001_ref="A.5.19, A.5.20, A.5.21, A.5.22",
hipaa_ref="§164.308(b) Business Associate Agreements",
gdpr_ref="Art. 28 Data Processing Agreements",
effort_days=10,
cost_usd=8_000,
implementation_notes=(
"Build vendor inventory spreadsheet. Tier vendors (Tier 1: PII access, "
"Tier 2: business data, Tier 3: no data). Execute DPAs for all processors (GDPR). "
"Execute BAAs for PHI processors (HIPAA). Annual security questionnaire for Tier 1."
),
status="Not Started",
owner="Legal/Security",
))
controls.append(build_control_domain(
domain_id="RISK-001",
name="Risk Assessment and Treatment",
description=(
"Formal risk assessment methodology. Risk register maintained. "
"Risk treatment decisions documented. Annual risk review cycle."
),
soc2_ref="CC3.1, CC3.2, CC3.3, CC3.4",
iso27001_ref="Clause 6.1.2, 6.1.3",
hipaa_ref="§164.308(a)(1) Security Risk Analysis",
gdpr_ref="Art. 32, Art. 35 DPIA",
effort_days=15,
cost_usd=12_000,
implementation_notes=(
"Document risk methodology (FAIR, NIST, ISO 27005). Maintain risk register. "
"HIPAA: formal security risk analysis required — not optional. "
"GDPR: DPIA required for high-risk processing activities. Annual refresh."
),
status="Not Started",
owner="CISO",
))
controls.append(build_control_domain(
domain_id="TRAIN-001",
name="Security Awareness Training",
description=(
"Annual security awareness training for all employees. "
"Role-specific training for high-risk roles. Phishing simulations. "
"Training completion tracking."
),
soc2_ref="CC1.4",
iso27001_ref="A.6.3, A.6.8",
hipaa_ref="§164.308(a)(5)",
gdpr_ref="Art. 39(1)(b)",
effort_days=5,
cost_usd=8_000,
implementation_notes=(
"Deploy security training platform (KnowBe4, Proofpoint, etc.). "
"Annual training required — track completion (100% target). "
"Quarterly phishing simulations. Role-specific training for devs (secure coding), "
"finance (BEC), support (social engineering)."
),
status="Not Started",
owner="HR/Security",
))
controls.append(build_control_domain(
domain_id="CHGMGMT-001",
name="Change Management",
description=(
"Formal change management process for production changes. "
"Code review requirements. Deployment approvals. Rollback procedures. "
"Change log maintained."
),
soc2_ref="CC8.1",
iso27001_ref="A.8.32",
hipaa_ref="§164.312(c)(1) Integrity controls",
gdpr_ref="Art. 25 Privacy by design",
effort_days=10,
cost_usd=5_000,
implementation_notes=(
"Document change management policy. Require peer review for all production changes. "
"Maintain audit trail in version control. No direct production access — "
"all changes via CI/CD pipeline."
),
status="In Progress",
owner="Engineering",
))
controls.append(build_control_domain(
domain_id="BCP-001",
name="Business Continuity and Disaster Recovery",
description=(
"Business continuity plan. Disaster recovery plan with defined RTO/RPO. "
"Backup procedures with tested restores. Failover capabilities."
),
soc2_ref="A1.1, A1.2, A1.3",
iso27001_ref="A.5.29, A.5.30",
hipaa_ref="§164.308(a)(7) Contingency Plan",
gdpr_ref="Art. 32(1)(c)",
effort_days=12,
cost_usd=15_000,
implementation_notes=(
"Define RTO (<4 hours) and RPO (<1 hour) targets. Configure automated backups. "
"Test restore quarterly — paper backups that aren't tested aren't backups. "
"Document DR runbook. Annual DR exercise."
),
status="In Progress",
owner="DevOps",
))
controls.append(build_control_domain(
domain_id="ASSET-001",
name="Asset Inventory and Classification",
description=(
"Complete inventory of hardware, software, and data assets. "
"Data classification scheme. Ownership assigned to all assets. "
"Regular reconciliation."
),
soc2_ref="CC6.1",
iso27001_ref="A.5.9, A.5.10, A.5.11, A.5.12, A.5.13",
hipaa_ref="§164.310(d) Device and Media Controls",
gdpr_ref="Art. 30 Records of Processing Activities",
effort_days=8,
cost_usd=5_000,
implementation_notes=(
"Build asset register (CMDB or spreadsheet at minimum). "
"Classify data: Public, Internal, Confidential, Restricted. "
"GDPR requires RoPA (Record of Processing Activities) — data map of all PII. "
"ISO 27001 requires SoA referencing asset inventory."
),
status="Not Started",
owner="IT/Security",
))
controls.append(build_control_domain(
domain_id="ENDPOINT-001",
name="Endpoint Security",
description=(
"EDR/antivirus on all managed endpoints. Device management (MDM). "
"Full disk encryption. Patch management. BYOD policy."
),
soc2_ref="CC6.8",
iso27001_ref="A.8.1, A.8.7",
hipaa_ref="§164.310(a)(2)(iv) Workstation security",
gdpr_ref="Art. 32(1)(a)",
effort_days=8,
cost_usd=20_000,
implementation_notes=(
"Deploy EDR (CrowdStrike, SentinelOne, or Microsoft Defender for Business). "
"Enable full disk encryption (FileVault/BitLocker). "
"MDM for device management. BYOD policy documented."
),
status="In Progress",
owner="IT",
))
controls.append(build_control_domain(
domain_id="POLICY-001",
name="Security Policies and Procedures",
description=(
"Documented security policies covering acceptable use, access control, "
"incident response, data classification, vendor management, etc. "
"Annual review cycle. Employee attestation."
),
soc2_ref="CC1.2, CC1.3",
iso27001_ref="A.5.1, A.5.2",
hipaa_ref="§164.308(a)(1) Security Management Process",
gdpr_ref="Art. 24 Responsibility of the controller",
effort_days=15,
cost_usd=10_000,
implementation_notes=(
"Minimum policy set: Information Security Policy, Acceptable Use, "
"Access Control, Incident Response, Data Classification, Password, "
"Change Management, Vendor Management, Business Continuity. "
"Use policy templates from GRC platform (Vanta/Drata)."
),
status="In Progress",
owner="CISO",
))
controls.append(build_control_domain(
domain_id="PRIV-001",
name="Privacy and Data Subject Rights",
description=(
"Privacy policy and notices. Data subject rights fulfilment process "
"(access, erasure, portability). Consent management. Cookie compliance. "
"Privacy by design in product development."
),
soc2_ref=None, # Not a SOC 2 requirement (unless Privacy TSC selected)
iso27001_ref="A.5.34",
hipaa_ref="§164.524 Access, §164.528 Accounting of Disclosures",
gdpr_ref="Art. 13, 14, 15–22 (Rights), Art. 25",
effort_days=20,
cost_usd=15_000,
implementation_notes=(
"GDPR: Update privacy policy, implement DSAR process (30-day SLA), "
"build deletion capability into product. Cookie consent (PECR/ePrivacy). "
"HIPAA: Patient rights for PHI access. "
"Consider OneTrust, Termly, or CookieYes for consent management."
),
status="Not Started",
owner="Legal/Product",
))
controls.append(build_control_domain(
domain_id="NET-001",
name="Network Security and Segmentation",
description=(
"Network segmentation (production vs. development vs. corporate). "
"Firewall rules. Intrusion detection. VPN or ZTNA for remote access."
),
soc2_ref="CC6.6, CC6.7",
iso27001_ref="A.8.20, A.8.21, A.8.22",
hipaa_ref="§164.312(e)(1) Transmission security",
gdpr_ref="Art. 32(1)(a)",
effort_days=12,
cost_usd=18_000,
implementation_notes=(
"Segment production from development. WAF in front of public applications. "
"Replace VPN with ZTNA for remote access (Series B+ consideration). "
"DDoS protection (Cloudflare or AWS Shield)."
),
status="In Progress",
owner="DevOps",
))
controls.append(build_control_domain(
domain_id="PENTEST-001",
name="Penetration Testing",
description=(
"Annual external penetration test by qualified third-party firm. "
"Finding remediation tracking. Results reviewed by leadership."
),
soc2_ref="CC7.1",
iso27001_ref="A.8.8",
hipaa_ref="§164.308(a)(8) Evaluation",
gdpr_ref="Art. 32(1)(d)",
effort_days=5,
cost_usd=25_000,
implementation_notes=(
"Scope: external attack surface, application, API, and optionally social engineering. "
"Budget $15–35K for a reputable firm. Track findings in risk register. "
"Re-test critical findings within 90 days. Share pentest summary with enterprise "
"customers on request (under NDA)."
),
status="Not Started",
owner="CISO",
))
return controls
# ─── Analysis ────────────────────────────────────────────────────────────────
def calculate_framework_coverage(controls: list[dict]) -> dict:
"""Calculate per-framework coverage statistics."""
coverage = {}
for fw in FRAMEWORKS:
applicable = [c for c in controls if fw in c["frameworks_applicable"]]
implemented = [c for c in applicable if c["status"] in ("Implemented", "Verified")]
in_progress = [c for c in applicable if c["status"] == "In Progress"]
not_started = [c for c in applicable if c["status"] == "Not Started"]
total_effort = sum(c["effort_days"] for c in applicable)
remaining_effort = sum(
c["effort_days"] for c in applicable
if c["status"] not in ("Implemented", "Verified")
)
total_cost = sum(c["cost_usd"] for c in applicable)
remaining_cost = sum(
c["cost_usd"] for c in applicable
if c["status"] not in ("Implemented", "Verified")
)
pct_complete = (len(implemented) / len(applicable) * 100) if applicable else 0
coverage[fw] = {
"framework": FRAMEWORKS[fw]["name"],
"total_controls": len(applicable),
"implemented": len(implemented),
"in_progress": len(in_progress),
"not_started": len(not_started),
"pct_complete": pct_complete,
"total_effort_days": total_effort,
"remaining_effort_days": remaining_effort,
"total_cost_usd": total_cost,
"remaining_cost_usd": remaining_cost,
"gap_controls": [c["name"] for c in not_started],
}
return coverage
def find_high_leverage_controls(controls: list[dict]) -> list[dict]:
"""Controls that satisfy the most frameworks — highest ROI to implement."""
multi_fw = [c for c in controls if c["framework_count"] >= 3
and c["status"] not in ("Implemented", "Verified")]
return sorted(multi_fw, key=lambda c: (-c["framework_count"], c["effort_days"]))
def estimate_roadmap(controls: list[dict], target_frameworks: list[str]) -> list[dict]:
"""
Generate an ordered implementation roadmap for target frameworks.
Prioritize: (1) controls blocking most frameworks, (2) quick wins (low effort).
"""
applicable = [c for c in controls
if any(fw in c["frameworks_applicable"] for fw in target_frameworks)
and c["status"] not in ("Implemented", "Verified")]
# Score: (frameworks_covered × 10) - (effort_days) → higher is better
for c in applicable:
fw_overlap = len([fw for fw in target_frameworks if fw in c["frameworks_applicable"]])
c["_priority_score"] = (fw_overlap * 10) - c["effort_days"]
return sorted(applicable, key=lambda c: -c["_priority_score"])
def fmt_dollars(amount: float) -> str:
if amount >= 1_000_000:
return f".1fM"
if amount >= 1_000:
return f".0fK"
return f".0f"
def status_icon(status: str) -> str:
icons = {
"Implemented": "✅",
"Verified": "✅",
"In Progress": "🔄",
"Not Started": "⬜",
"Planned": "📋",
}
return icons.get(status, "❓")
# ─── Display ─────────────────────────────────────────────────────────────────
def print_header():
print("\n" + "=" * 80)
print(" CISO COMPLIANCE TRACKER — Multi-Framework Coverage")
print(f" Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 80)
def print_framework_summary(coverage: dict):
print("\n📋 FRAMEWORK COVERAGE SUMMARY")
print("-" * 80)
header = f"{'Framework':<20} {'Done':<6} {'WIP':<5} {'Gap':<5} {'Complete':<10} {'Remain Cost':<14} {'Remain Days'}"
print(header)
print("-" * 80)
for fw_id, data in coverage.items():
pct = f"{data['pct_complete']:.0f}%"
print(
f"{data['framework']:<20} {data['implemented']:<6} {data['in_progress']:<5} "
f"{data['not_started']:<5} {pct:<10} {fmt_dollars(data['remaining_cost_usd']):<14} "
f"{data['remaining_effort_days']} days"
)
def print_control_table(controls: list[dict], framework_filter: Optional[str] = None):
filtered = controls
if framework_filter:
filtered = [c for c in controls if framework_filter in c["frameworks_applicable"]]
title = f"CONTROL DOMAINS"
if framework_filter:
title += f" — {FRAMEWORKS[framework_filter]['name']}"
print(f"\n🔧 {title}")
print("-" * 90)
header = f"{'ID':<14} {'Control Name':<30} {'Frameworks':<8} {'Effort':<8} {'Cost':<10} {'Status'}"
print(header)
print("-" * 90)
for c in filtered:
fw_badges = "/".join(
fw.upper()[:3] for fw in ["soc2", "iso27001", "hipaa", "gdpr"]
if fw in c["frameworks_applicable"]
)
icon = status_icon(c["status"])
print(
f"{c['domain_id']:<14} {c['name'][:29]:<30} {fw_badges:<8} "
f"{c['effort_days']:>3}d {fmt_dollars(c['cost_usd']):<10} {icon} {c['status']}"
)
def print_gap_analysis(coverage: dict):
print("\n⚠️ GAP ANALYSIS — Controls Not Yet Started")
print("-" * 70)
for fw_id, data in coverage.items():
if data["gap_controls"]:
print(f"\n {data['framework']} — {len(data['gap_controls'])} gaps:")
for gap in data["gap_controls"]:
print(f" • {gap}")
def print_high_leverage(controls: list[dict]):
hl = find_high_leverage_controls(controls)
print(f"\n🎯 HIGH-LEVERAGE CONTROLS — Implement Once, Satisfy Multiple Frameworks")
print("-" * 70)
print(f"{'Control':<30} {'Frameworks':<35} {'Effort':<8} {'Cost'}")
print("-" * 70)
for c in hl:
fw_list = " + ".join(FRAMEWORKS[fw]["name"] for fw in c["frameworks_applicable"])
print(
f"{c['name'][:29]:<30} {fw_list[:34]:<35} "
f"{c['effort_days']:>3}d {fmt_dollars(c['cost_usd'])}"
)
def print_roadmap(controls: list[dict], target_frameworks: list[str]):
ordered = estimate_roadmap(controls, target_frameworks)
fw_names = " + ".join(FRAMEWORKS[fw]["name"] for fw in target_frameworks)
print(f"\n🗺️ IMPLEMENTATION ROADMAP — {fw_names}")
print("-" * 80)
print("Priority order: most framework coverage first, then quick wins")
print()
cumulative_days = 0
cumulative_cost = 0
for i, c in enumerate(ordered, 1):
cumulative_days += c["effort_days"]
cumulative_cost += c["cost_usd"]
fw_badges = ", ".join(
FRAMEWORKS[fw]["name"] for fw in target_frameworks
if fw in c["frameworks_applicable"]
)
print(f" {i:>2}. {c['name']}")
print(f" Frameworks: {fw_badges}")
print(f" Effort: {c['effort_days']} days | Cost: {fmt_dollars(c['cost_usd'])} "
f"| Cumulative: {cumulative_days}d / {fmt_dollars(cumulative_cost)}")
if c.get("owner"):
print(f" Owner: {c['owner']}")
print()
def print_framework_profiles():
print("\n💼 FRAMEWORK PROFILES")
print("-" * 70)
for fw_id, fw in FRAMEWORKS.items():
print(f"\n {fw['name']} ({fw_id.upper()})")
print(f" Timeline: ~{fw['typical_timeline_months']} months")
print(f" First-year cost: {fmt_dollars(fw['typical_cost_usd'])}")
print(f" Annual maintenance: {fmt_dollars(fw['annual_maintenance_usd'])}/yr")
print(f" Business value: {fw['business_value']}")
print(f" Required for: {', '.join(fw['mandatory_for'])}")
def export_csv(controls: list[dict], filepath: str):
fields = [
"domain_id", "name", "frameworks_applicable", "framework_count",
"effort_days", "cost_usd", "status", "owner", "target_date",
"soc2_ref", "iso27001_ref", "hipaa_ref", "gdpr_ref", "implementation_notes"
]
with open(filepath, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
for c in controls:
row = {k: c.get(k, "") for k in fields}
row["frameworks_applicable"] = ", ".join(c["frameworks_applicable"])
row["soc2_ref"] = c["references"].get("soc2", "")
row["iso27001_ref"] = c["references"].get("iso27001", "")
row["hipaa_ref"] = c["references"].get("hipaa", "")
row["gdpr_ref"] = c["references"].get("gdpr", "")
writer.writerow(row)
print(f"✅ Exported {len(controls)} controls to {filepath}")
# ─── Main ────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="CISO Compliance Tracker — Multi-framework coverage and roadmap"
)
parser.add_argument("--json", action="store_true", help="Output JSON")
parser.add_argument("--csv", metavar="FILE", help="Export CSV to file")
parser.add_argument(
"--framework", metavar="FRAMEWORK",
choices=list(FRAMEWORKS.keys()),
help="Filter to single framework (soc2, iso27001, hipaa, gdpr)"
)
parser.add_argument("--gap-analysis", action="store_true", help="Show gap analysis")
parser.add_argument("--roadmap", metavar="FRAMEWORKS",
help="Sequenced roadmap for frameworks e.g. 'soc2,iso27001'")
parser.add_argument("--profiles", action="store_true", help="Show framework profiles")
parser.add_argument("--leverage", action="store_true", help="Show high-leverage controls")
args = parser.parse_args()
controls = load_control_library()
coverage = calculate_framework_coverage(controls)
if args.json:
output = {
"generated": datetime.now().isoformat(),
"frameworks": FRAMEWORKS,
"coverage": coverage,
"controls": controls,
}
print(json.dumps(output, indent=2, default=str))
return
if args.csv:
export_csv(controls, args.csv)
return
print_header()
if args.profiles:
print_framework_profiles()
return
if args.roadmap:
target_fws = [fw.strip() for fw in args.roadmap.split(",") if fw.strip() in FRAMEWORKS]
if not target_fws:
print(f"Unknown frameworks. Valid: {', '.join(FRAMEWORKS.keys())}")
sys.exit(1)
print_framework_summary(coverage)
print_roadmap(controls, target_fws)
return
print_framework_summary(coverage)
print_control_table(controls, args.framework)
if args.gap_analysis:
print_gap_analysis(coverage)
if args.leverage:
print_high_leverage(controls)
if not any([args.framework, args.gap_analysis, args.leverage]):
print_high_leverage(controls)
print_gap_analysis(coverage)
print("\n💡 NEXT STEPS")
print(" --roadmap soc2,iso27001 Priority order for dual-framework")
print(" --framework hipaa HIPAA-only control view")
print(" --gap-analysis What's not started")
print(" --leverage Controls covering most frameworks")
print(" --profiles Framework timelines and costs")
print(" --csv controls.csv Export for stakeholder review")
print()
if __name__ == "__main__":
main()
FILE:scripts/risk_quantifier.py
#!/usr/bin/env python3
"""
CISO Risk Quantifier
====================
Quantifies security risks in business terms using the FAIR model.
Calculates ALE (Annual Loss Expectancy) and prioritizes by expected annual loss.
Usage:
python risk_quantifier.py # Run with sample data
python risk_quantifier.py --json # Output JSON
python risk_quantifier.py --csv output.csv # Export CSV
python risk_quantifier.py --budget 500000 # Show what fits in budget
python risk_quantifier.py --add # Interactive risk entry
"""
import json
import csv
import sys
import os
import argparse
from datetime import datetime
from typing import Optional
# ─── Data Model ─────────────────────────────────────────────────────────────
RISK_CATEGORIES = [
"Data Breach",
"Ransomware / Extortion",
"Insider Threat",
"Third-Party / Supply Chain",
"Application Vulnerability",
"Cloud Misconfiguration",
"Social Engineering",
"Physical Security",
"Business Email Compromise",
"DDoS / Availability",
]
BUSINESS_IMPACT_TYPES = [
"Revenue Loss",
"Regulatory Fine",
"Legal / Litigation",
"Reputational Damage",
"Recovery / Remediation Cost",
"Customer Churn",
"Business Interruption",
]
MITIGATION_STATUSES = ["None", "Planned", "In Progress", "Mitigated", "Accepted"]
def build_risk(
name: str,
category: str,
description: str,
asset_value: float,
exposure_factor: float, # 0.0–1.0: fraction of asset value lost in breach
annual_rate: float, # ARO: expected incidents per year (0.01 = once per 100 years)
mitigation_cost: float,
mitigation_effectiveness: float, # 0.0–1.0: fraction of risk reduced by control
mitigation_status: str,
business_impacts: dict, # {impact_type: dollar_amount}
notes: str = "",
) -> dict:
"""Construct a risk record with calculated metrics."""
sle = asset_value * exposure_factor # Single Loss Expectancy
ale = sle * annual_rate # Annual Loss Expectancy (inherent)
mitigated_ale = ale * (1 - mitigation_effectiveness) # Residual after mitigation
mitigation_roi = ((ale - mitigated_ale - mitigation_cost) / mitigation_cost * 100
if mitigation_cost > 0 else 0)
total_business_impact = sum(business_impacts.values())
return {
"name": name,
"category": category,
"description": description,
"asset_value": asset_value,
"exposure_factor": exposure_factor,
"annual_rate": annual_rate,
"mitigation_cost": mitigation_cost,
"mitigation_effectiveness": mitigation_effectiveness,
"mitigation_status": mitigation_status,
"business_impacts": business_impacts,
"notes": notes,
# Calculated
"sle": sle,
"ale": ale,
"mitigated_ale": mitigated_ale,
"mitigation_roi_pct": mitigation_roi,
"total_business_impact": total_business_impact,
"priority_score": ale, # Primary sort key
}
# ─── Sample Data ─────────────────────────────────────────────────────────────
def load_sample_risks() -> list[dict]:
"""
Sample risk register for a Series B SaaS company with ~$15M ARR,
~50K customer records, B2B enterprise focus.
"""
risks = []
risks.append(build_risk(
name="Customer Database Breach",
category="Data Breach",
description=(
"Unauthorized access to production database containing 50K+ customer records "
"including PII (name, email, company, payment method). Attack vector: SQL injection, "
"compromised credentials, or insider access."
),
asset_value=5_000_000, # Value of customer database (revenue impact + regulatory)
exposure_factor=0.30, # ~30% of asset value lost in a breach event
annual_rate=0.12, # ~12% chance per year (based on Verizon DBIR industry data)
mitigation_cost=45_000, # WAF + DAST + DB activity monitoring annual cost
mitigation_effectiveness=0.80,
mitigation_status="In Progress",
business_impacts={
"Regulatory Fine": 85_000, # GDPR/CCPA exposure
"Legal / Litigation": 150_000, # Class action exposure
"Customer Churn": 300_000, # Lost ARR from breach-triggered churn
"Reputational Damage": 200_000, # Brand impact / deal loss
"Recovery / Remediation Cost": 65_000,
},
notes="SOC 2 Type II controls partially address. Next step: DB activity monitoring.",
))
risks.append(build_risk(
name="Ransomware Attack",
category="Ransomware / Extortion",
description=(
"Ransomware encrypts production systems. Average ransom demand for a "
"Series B company is $350K–$800K. Recovery without ransom payment: 2–6 weeks downtime. "
"Attack vector: phishing email with malicious attachment, RDP exposure."
),
asset_value=3_500_000,
exposure_factor=0.25,
annual_rate=0.15,
mitigation_cost=60_000, # EDR + email security + backup hardening
mitigation_effectiveness=0.85,
mitigation_status="Planned",
business_impacts={
"Business Interruption": 450_000, # 4 weeks downtime × $112K/week revenue
"Recovery / Remediation Cost": 180_000,
"Customer Churn": 125_000,
"Revenue Loss": 75_000,
},
notes="Offline, tested backups reduce recovery time and eliminate ransom pressure.",
))
risks.append(build_risk(
name="Privileged Insider Data Theft",
category="Insider Threat",
description=(
"Disgruntled or financially motivated employee with elevated access exfiltrates "
"customer data, IP, or trade secrets. Detection is typically slow (median: 197 days "
"per IBM Cost of Data Breach Report)."
),
asset_value=2_800_000,
exposure_factor=0.20,
annual_rate=0.08,
mitigation_cost=35_000, # DLP + UEBA + PAM
mitigation_effectiveness=0.65,
mitigation_status="None",
business_impacts={
"Legal / Litigation": 120_000,
"Customer Churn": 90_000,
"Reputational Damage": 75_000,
"Recovery / Remediation Cost": 40_000,
},
notes="No DLP or UEBA currently deployed. Highest detection gap.",
))
risks.append(build_risk(
name="Critical SaaS Vendor Breach (Supply Chain)",
category="Third-Party / Supply Chain",
description=(
"A critical SaaS vendor (e.g., Salesforce, Slack, AWS, GitHub) suffers a breach "
"that compromises data entrusted to them or disrupts your operations. You have "
"limited control but full liability to customers."
),
asset_value=2_200_000,
exposure_factor=0.15,
annual_rate=0.18,
mitigation_cost=20_000, # Vendor risk assessment program
mitigation_effectiveness=0.40, # Limited — you can't control vendor security
mitigation_status="Planned",
business_impacts={
"Business Interruption": 95_000,
"Customer Churn": 75_000,
"Reputational Damage": 50_000,
"Recovery / Remediation Cost": 30_000,
},
notes="Third-party risk is partially transferable via contractual SLAs and cyber insurance.",
))
risks.append(build_risk(
name="Business Email Compromise (BEC)",
category="Business Email Compromise",
description=(
"Attacker impersonates CEO, CFO, or vendor to redirect wire transfers, gift card "
"purchases, or payroll. Median BEC loss: $125K. FBI IC3 reports BEC as #1 "
"cybercrime by financial loss."
),
asset_value=500_000,
exposure_factor=0.40,
annual_rate=0.30,
mitigation_cost=12_000, # Email authentication (DMARC) + training + callback procedures
mitigation_effectiveness=0.90,
mitigation_status="In Progress",
business_impacts={
"Revenue Loss": 125_000, # Direct financial theft (often unrecoverable)
"Recovery / Remediation Cost": 25_000,
"Legal / Litigation": 15_000,
},
notes="DMARC deployed. Need to enforce wire transfer callback procedures.",
))
risks.append(build_risk(
name="Cloud Misconfiguration — S3 / Storage Exposure",
category="Cloud Misconfiguration",
description=(
"Public exposure of S3 buckets, GCS buckets, or Azure Blob storage containing "
"sensitive data. One of the most common causes of data breaches. Often undetected "
"for months. 2023 IBM study: 82% of breaches involved data stored in cloud."
),
asset_value=1_800_000,
exposure_factor=0.20,
annual_rate=0.20,
mitigation_cost=18_000, # CSPM tool + IaC scanning
mitigation_effectiveness=0.90,
mitigation_status="Planned",
business_impacts={
"Regulatory Fine": 60_000,
"Reputational Damage": 120_000,
"Legal / Litigation": 45_000,
"Recovery / Remediation Cost": 35_000,
},
notes="No CSPM currently. High frequency, high detectability, low mitigation cost.",
))
risks.append(build_risk(
name="Credential Stuffing — Customer Accounts",
category="Application Vulnerability",
description=(
"Attackers use leaked credential lists to compromise customer accounts. "
"Account takeover leads to data theft, fraudulent transactions, and support burden. "
"16 billion credentials available on darknet as of 2024."
),
asset_value=1_200_000,
exposure_factor=0.12,
annual_rate=0.40,
mitigation_cost=15_000, # MFA + rate limiting + bot detection
mitigation_effectiveness=0.95,
mitigation_status="In Progress",
business_impacts={
"Customer Churn": 80_000,
"Revenue Loss": 45_000,
"Recovery / Remediation Cost": 19_000,
"Reputational Damage": 30_000,
},
notes="MFA available but optional. Enforcing MFA cuts this risk by ~99%.",
))
risks.append(build_risk(
name="Phishing — Employee Credential Compromise",
category="Social Engineering",
description=(
"Employee clicks phishing link, surrenders credentials. Without MFA, "
"this provides full access to email, SaaS apps, and potentially production. "
"Phishing is the #1 attack vector in the Verizon DBIR."
),
asset_value=1_500_000,
exposure_factor=0.15,
annual_rate=0.35,
mitigation_cost=25_000, # MFA + security awareness training + email security
mitigation_effectiveness=0.92,
mitigation_status="In Progress",
business_impacts={
"Business Interruption": 65_000,
"Customer Churn": 55_000,
"Recovery / Remediation Cost": 45_000,
"Reputational Damage": 60_000,
},
notes="Primary vector for ransomware and BEC. MFA is the single highest-ROI control.",
))
risks.append(build_risk(
name="Application API Vulnerability",
category="Application Vulnerability",
description=(
"Unauthenticated or improperly authorized API endpoint exposes customer data "
"or administrative functions. OWASP API Security Top 10 — broken object-level "
"authorization is the most common API vulnerability."
),
asset_value=2_000_000,
exposure_factor=0.18,
annual_rate=0.15,
mitigation_cost=30_000, # DAST + API gateway + code review
mitigation_effectiveness=0.75,
mitigation_status="Planned",
business_impacts={
"Regulatory Fine": 70_000,
"Customer Churn": 90_000,
"Reputational Damage": 100_000,
"Legal / Litigation": 60_000,
},
notes="Need automated API security testing in CI/CD pipeline.",
))
risks.append(build_risk(
name="DDoS Attack — Production Service",
category="DDoS / Availability",
description=(
"Distributed denial-of-service attack renders production service unavailable. "
"Average DDoS duration: 4–8 hours. Enterprise SLA breach triggers contractual "
"penalties. Increasingly used as extortion or distraction tactic."
),
asset_value=1_000_000,
exposure_factor=0.10,
annual_rate=0.25,
mitigation_cost=15_000, # CDN with DDoS protection (Cloudflare, AWS Shield)
mitigation_effectiveness=0.85,
mitigation_status="Mitigated",
business_impacts={
"Business Interruption": 45_000,
"Customer Churn": 30_000,
"Revenue Loss": 25_000,
},
notes="Cloudflare deployed. Residual risk from very large volumetric attacks.",
))
return risks
# ─── Analysis & Reporting ────────────────────────────────────────────────────
def calculate_portfolio_summary(risks: list[dict]) -> dict:
"""Aggregate portfolio-level metrics."""
total_inherent_ale = sum(r["ale"] for r in risks)
total_mitigated_ale = sum(r["mitigated_ale"] for r in risks)
total_mitigation_cost = sum(r["mitigation_cost"] for r in risks)
risk_reduction = total_inherent_ale - total_mitigated_ale
portfolio_roi = ((risk_reduction - total_mitigation_cost) / total_mitigation_cost * 100
if total_mitigation_cost > 0 else 0)
by_category = {}
for r in risks:
cat = r["category"]
if cat not in by_category:
by_category[cat] = {"count": 0, "total_ale": 0.0}
by_category[cat]["count"] += 1
by_category[cat]["total_ale"] += r["ale"]
by_status = {}
for r in risks:
status = r["mitigation_status"]
by_status[status] = by_status.get(status, 0) + 1
return {
"total_risks": len(risks),
"total_inherent_ale": total_inherent_ale,
"total_mitigated_ale": total_mitigated_ale,
"total_risk_reduction": risk_reduction,
"total_mitigation_cost": total_mitigation_cost,
"portfolio_roi_pct": portfolio_roi,
"by_category": dict(sorted(by_category.items(), key=lambda x: -x[1]["total_ale"])),
"by_mitigation_status": by_status,
}
def prioritize_risks(risks: list[dict], budget: Optional[float] = None) -> list[dict]:
"""Return risks sorted by ALE. If budget given, show what fits."""
sorted_risks = sorted(risks, key=lambda r: -r["ale"])
if budget is None:
return sorted_risks
# Greedy budget allocation by ROI
actionable = [r for r in sorted_risks if r["mitigation_status"] in ("None", "Planned")
and r["mitigation_cost"] > 0]
actionable.sort(key=lambda r: -r["mitigation_roi_pct"])
allocated = []
remaining = budget
for risk in actionable:
if risk["mitigation_cost"] <= remaining:
allocated.append(risk)
remaining -= risk["mitigation_cost"]
return allocated
def fmt_dollars(amount: float) -> str:
"""Format a dollar amount."""
if amount >= 1_000_000:
return f".2fM"
if amount >= 1_000:
return f".0fK"
return f".0f"
def fmt_pct(value: float) -> str:
return f"{value:.1f}%"
def severity_label(ale: float) -> str:
if ale >= 200_000:
return "CRITICAL"
if ale >= 75_000:
return "HIGH"
if ale >= 25_000:
return "MEDIUM"
return "LOW"
def severity_color(label: str) -> str:
"""ANSI color codes."""
colors = {
"CRITICAL": "\033[91m", # Red
"HIGH": "\033[93m", # Yellow
"MEDIUM": "\033[94m", # Blue
"LOW": "\033[92m", # Green
}
return colors.get(label, "") + label + "\033[0m"
# ─── Display ─────────────────────────────────────────────────────────────────
def print_header():
print("\n" + "=" * 80)
print(" CISO RISK QUANTIFIER — Security Risk Portfolio")
print(f" Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 80)
def print_portfolio_summary(summary: dict):
print("\n📊 PORTFOLIO SUMMARY")
print("-" * 60)
print(f" Total risks tracked: {summary['total_risks']}")
print(f" Total inherent ALE: {fmt_dollars(summary['total_inherent_ale'])}/yr")
print(f" Total ALE after mitigations: {fmt_dollars(summary['total_mitigated_ale'])}/yr")
print(f" Risk reduction from controls: {fmt_dollars(summary['total_risk_reduction'])}/yr")
print(f" Total mitigation spend: {fmt_dollars(summary['total_mitigation_cost'])}/yr")
print(f" Portfolio ROI: {fmt_pct(summary['portfolio_roi_pct'])}")
print()
print(" Risk by Category (sorted by ALE):")
for cat, data in summary["by_category"].items():
print(f" {cat:<35} {data['count']} risks ALE: {fmt_dollars(data['total_ale'])}/yr")
print()
print(" Mitigation Status:")
for status, count in summary["by_mitigation_status"].items():
print(f" {status:<20} {count} risks")
def print_risk_table(risks: list[dict], title: str = "RISK REGISTER"):
print(f"\n🎯 {title}")
print("-" * 80)
header = f"{'#':<3} {'Risk Name':<35} {'Severity':<10} {'ALE/yr':<12} {'Mitig Cost':<12} {'ROI':<8} {'Status':<12}"
print(header)
print("-" * 80)
for i, risk in enumerate(risks, 1):
sev = severity_label(risk["ale"])
sev_str = sev.ljust(10)
roi = fmt_pct(risk["mitigation_roi_pct"]) if risk["mitigation_cost"] > 0 else "N/A"
print(
f"{i:<3} {risk['name'][:34]:<35} {sev_str} "
f"{fmt_dollars(risk['ale']):<12} {fmt_dollars(risk['mitigation_cost']):<12} "
f"{roi:<8} {risk['mitigation_status']}"
)
def print_risk_detail(risk: dict, index: int):
sev = severity_label(risk["ale"])
print(f"\n{'─' * 70}")
print(f" #{index} — {risk['name']} [{sev}]")
print(f"{'─' * 70}")
print(f" Category: {risk['category']}")
print(f" Description: {risk['description'][:120]}...")
print()
print(f" RISK CALCULATION:")
print(f" Asset Value: {fmt_dollars(risk['asset_value'])}")
print(f" Exposure Factor: {fmt_pct(risk['exposure_factor'] * 100)}")
print(f" Single Loss Expectancy: {fmt_dollars(risk['sle'])}")
print(f" Annual Rate (ARO): {risk['annual_rate']:.2f}x/year")
print(f" Annual Loss Expectancy: {fmt_dollars(risk['ale'])}/yr ← INHERENT RISK")
print()
print(f" MITIGATION:")
print(f" Mitigation Cost: {fmt_dollars(risk['mitigation_cost'])}/yr")
print(f" Effectiveness: {fmt_pct(risk['mitigation_effectiveness'] * 100)}")
print(f" Residual ALE: {fmt_dollars(risk['mitigated_ale'])}/yr")
print(f" Mitigation ROI: {fmt_pct(risk['mitigation_roi_pct'])}")
print(f" Status: {risk['mitigation_status']}")
print()
print(f" BUSINESS IMPACT BREAKDOWN:")
for impact_type, amount in risk["business_impacts"].items():
print(f" {impact_type:<30} {fmt_dollars(amount)}")
print(f" {'TOTAL':<30} {fmt_dollars(risk['total_business_impact'])}")
if risk["notes"]:
print(f"\n NOTES: {risk['notes']}")
def print_board_summary(risks: list[dict], summary: dict):
"""One-page board-ready summary."""
print("\n" + "═" * 80)
print(" BOARD SECURITY REPORT — Risk Summary")
print("═" * 80)
critical = [r for r in risks if severity_label(r["ale"]) == "CRITICAL"]
high = [r for r in risks if severity_label(r["ale"]) == "HIGH"]
medium = [r for r in risks if severity_label(r["ale"]) == "MEDIUM"]
low = [r for r in risks if severity_label(r["ale"]) == "LOW"]
print(f"\n RISK EXPOSURE SUMMARY")
print(f" ┌─────────────┬────────┬──────────────┐")
print(f" │ Severity │ Count │ Total ALE/yr │")
print(f" ├─────────────┼────────┼──────────────┤")
for label, group in [("Critical", critical), ("High", high), ("Medium", medium), ("Low", low)]:
ale = sum(r["ale"] for r in group)
print(f" │ {label:<11} │ {len(group):<6} │ {fmt_dollars(ale):<12} │")
print(f" └─────────────┴────────┴──────────────┘")
print(f"\n TOTAL INHERENT RISK: {fmt_dollars(summary['total_inherent_ale'])}/yr")
print(f" SECURITY INVESTMENT: {fmt_dollars(summary['total_mitigation_cost'])}/yr")
print(f" RESIDUAL RISK: {fmt_dollars(summary['total_mitigated_ale'])}/yr")
print(f" RISK REDUCTION: {fmt_dollars(summary['total_risk_reduction'])}/yr")
print(f" PORTFOLIO ROI: {fmt_pct(summary['portfolio_roi_pct'])}")
print(f"\n TOP 3 RISKS BY EXPECTED ANNUAL LOSS:")
top3 = sorted(risks, key=lambda r: -r["ale"])[:3]
for i, risk in enumerate(top3, 1):
print(f" {i}. {risk['name']}: {fmt_dollars(risk['ale'])}/yr expected annual loss")
print(f" Mitigation: {fmt_dollars(risk['mitigation_cost'])}/yr | "
f"Status: {risk['mitigation_status']}")
unmitigated = [r for r in risks if r["mitigation_status"] == "None"]
if unmitigated:
print(f"\n ⚠️ UNMITIGATED RISKS ({len(unmitigated)}):")
for r in sorted(unmitigated, key=lambda x: -x["ale"]):
print(f" • {r['name']}: {fmt_dollars(r['ale'])}/yr — Action required")
def export_csv(risks: list[dict], filepath: str):
fields = [
"name", "category", "asset_value", "exposure_factor", "annual_rate",
"sle", "ale", "mitigation_cost", "mitigation_effectiveness",
"mitigated_ale", "mitigation_roi_pct", "mitigation_status", "notes"
]
with open(filepath, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
for risk in risks:
row = {k: risk.get(k, "") for k in fields}
writer.writerow(row)
print(f"✅ Exported {len(risks)} risks to {filepath}")
def export_json(risks: list[dict]) -> str:
return json.dumps(risks, indent=2, default=str)
# ─── Interactive Entry ───────────────────────────────────────────────────────
def interactive_add_risk() -> dict:
"""Interactive CLI for adding a new risk."""
print("\n── ADD NEW RISK ──────────────────────────────────────")
name = input("Risk name: ").strip()
print(f"Category options: {', '.join(RISK_CATEGORIES)}")
category = input("Category: ").strip()
description = input("Description (brief): ").strip()
print("\nAsset valuation:")
asset_value = float(input(" Asset value ($): ").replace(",", "").replace("$", ""))
exposure_factor = float(input(" Exposure factor (0.0–1.0, fraction of value lost): "))
annual_rate = float(input(" Annual rate of occurrence (e.g., 0.10 = once per 10 years): "))
print("\nMitigation:")
mitigation_cost = float(input(" Mitigation cost ($/yr): ").replace(",", "").replace("$", ""))
mitigation_effectiveness = float(input(" Mitigation effectiveness (0.0–1.0): "))
print(f"Status options: {', '.join(MITIGATION_STATUSES)}")
mitigation_status = input(" Status: ").strip()
print("\nBusiness impacts (enter 0 to skip):")
business_impacts = {}
for impact_type in BUSINESS_IMPACT_TYPES:
val = input(f" {impact_type} ($): ").replace(",", "").replace("$", "")
amount = float(val) if val else 0
if amount > 0:
business_impacts[impact_type] = amount
notes = input("\nNotes: ").strip()
return build_risk(
name=name,
category=category,
description=description,
asset_value=asset_value,
exposure_factor=exposure_factor,
annual_rate=annual_rate,
mitigation_cost=mitigation_cost,
mitigation_effectiveness=mitigation_effectiveness,
mitigation_status=mitigation_status,
business_impacts=business_impacts,
notes=notes,
)
# ─── Main ────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="CISO Risk Quantifier — Quantify security risks in business terms"
)
parser.add_argument("--json", action="store_true", help="Output full JSON")
parser.add_argument("--csv", metavar="FILE", help="Export CSV to file")
parser.add_argument("--budget", type=float, metavar="DOLLARS",
help="Show recommended mitigations within budget")
parser.add_argument("--board", action="store_true", help="Show board-ready summary only")
parser.add_argument("--detail", action="store_true", help="Show detailed risk breakdowns")
parser.add_argument("--add", action="store_true", help="Interactively add a risk")
args = parser.parse_args()
risks = load_sample_risks()
if args.add:
new_risk = interactive_add_risk()
risks.append(new_risk)
print(f"\n✅ Added risk: {new_risk['name']} | ALE: {fmt_dollars(new_risk['ale'])}/yr")
# Sort by ALE descending
risks_sorted = sorted(risks, key=lambda r: -r["ale"])
summary = calculate_portfolio_summary(risks_sorted)
if args.json:
output = {
"generated": datetime.now().isoformat(),
"summary": summary,
"risks": risks_sorted,
}
print(json.dumps(output, indent=2, default=str))
return
if args.csv:
export_csv(risks_sorted, args.csv)
return
print_header()
if args.board:
print_board_summary(risks_sorted, summary)
return
print_portfolio_summary(summary)
print_risk_table(risks_sorted)
if args.detail:
for i, risk in enumerate(risks_sorted, 1):
print_risk_detail(risk, i)
if args.budget:
recommended = prioritize_risks(risks_sorted, args.budget)
print(f"\n💰 BUDGET ALLOCATION — ,.0f")
print(f" Recommended mitigations (sorted by ROI):")
if recommended:
for r in recommended:
print(f" • {r['name']}: {fmt_dollars(r['mitigation_cost'])}/yr "
f"| ALE reduction: {fmt_dollars(r['ale'] - r['mitigated_ale'])}/yr "
f"| ROI: {fmt_pct(r['mitigation_roi_pct'])}")
else:
print(" No actionable mitigations fit within budget.")
print_board_summary(risks_sorted, summary)
print("\n💡 NEXT STEPS")
print(" 1. Run `--detail` to see full breakdown of each risk")
print(" 2. Run `--budget 200000` to see what you can mitigate with a given budget")
print(" 3. Run `--board` for a board-ready one-page summary")
print(" 4. Run `--csv risks.csv` to export for stakeholder review")
print(" 5. Run `--add` to interactively add risks to the register")
print()
if __name__ == "__main__":
main()
Revenue leadership for B2B SaaS companies. Revenue forecasting, sales model design, pricing strategy, net revenue retention, and sales team scaling. Use when...
---
name: "cro-advisor"
description: "Revenue leadership for B2B SaaS companies. Revenue forecasting, sales model design, pricing strategy, net revenue retention, and sales team scaling. Use when designing the revenue engine, setting quotas, modeling NRR, evaluating pricing, building board forecasts, or when user mentions CRO, chief revenue officer, revenue strategy, sales model, ARR growth, NRR, expansion revenue, churn, pricing strategy, or sales capacity."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: cro-leadership
updated: 2026-03-05
python-tools: revenue_forecast_model.py, churn_analyzer.py
frameworks: sales-playbook, pricing-strategy, nrr-playbook
---
# CRO Advisor
Revenue frameworks for building predictable, scalable revenue engines — from $1M ARR to $100M and beyond.
## Keywords
CRO, chief revenue officer, revenue strategy, ARR, MRR, sales model, pipeline, revenue forecasting, pricing strategy, net revenue retention, NRR, gross revenue retention, GRR, expansion revenue, upsell, cross-sell, churn, customer success, sales capacity, quota, ramp, territory design, MEDDPICC, PLG, product-led growth, sales-led growth, enterprise sales, SMB, self-serve, value-based pricing, usage-based pricing, ICP, ideal customer profile, revenue board reporting, sales cycle, CAC payback, magic number
## Quick Start
### Revenue Forecasting
```bash
python scripts/revenue_forecast_model.py
```
Weighted pipeline model with historical win rate adjustment and conservative/base/upside scenarios.
### Churn & Retention Analysis
```bash
python scripts/churn_analyzer.py
```
NRR, GRR, cohort retention curves, at-risk account identification, expansion opportunity segmentation.
## Diagnostic Questions
Ask these before any framework:
**Revenue Health**
- What's your NRR? If below 100%, everything else is a leaky bucket.
- What percentage of ARR comes from expansion vs. new logo?
- What's your GRR (retention floor without expansion)?
**Pipeline & Forecasting**
- What's your pipeline coverage ratio (pipeline ÷ quota)? Under 3x is a problem.
- Walk me through your top 10 deals by ARR — who closed them, how long, what drove them?
- What's your stage-by-stage conversion rate? Where do deals die?
**Sales Team**
- What % of your sales team hit quota last quarter?
- What's average ramp time before a new AE is quota-attaining?
- What's the sales cycle variance by segment? High variance = unpredictable forecasts.
**Pricing**
- How do customers articulate the value they get? What outcome do you deliver?
- When did you last raise prices? What happened to win rate?
- If fewer than 20% of prospects push back on price, you're underpriced.
## Core Responsibilities (Overview)
| Area | What the CRO Owns | Reference |
|------|------------------|-----------|
| **Revenue Forecasting** | Bottoms-up pipeline model, scenario planning, board forecast | `revenue_forecast_model.py` |
| **Sales Model** | PLG vs. sales-led vs. hybrid, team structure, stage definitions | `references/sales_playbook.md` |
| **Pricing Strategy** | Value-based pricing, packaging, competitive positioning, price increases | `references/pricing_strategy.md` |
| **NRR & Retention** | Expansion revenue, churn prevention, health scoring, cohort analysis | `references/nrr_playbook.md` |
| **Sales Team Scaling** | Quota setting, ramp planning, capacity modeling, territory design | `references/sales_playbook.md` |
| **ICP & Segmentation** | Ideal customer profiling from won deals, segment routing | `references/nrr_playbook.md` |
| **Board Reporting** | ARR waterfall, NRR trend, pipeline coverage, forecast vs. actual | `revenue_forecast_model.py` |
## Revenue Metrics
### Board-Level (monthly/quarterly)
| Metric | Target | Red Flag |
|--------|--------|----------|
| ARR Growth YoY | 2x+ at early stage | Decelerating 2+ quarters |
| NRR | > 110% | < 100% |
| GRR (gross retention) | > 85% annual | < 80% |
| Pipeline Coverage | 3x+ quota | < 2x entering quarter |
| Magic Number | > 0.75 | < 0.5 (fix unit economics before spending more) |
| CAC Payback | < 18 months | > 24 months |
| Quota Attainment % | 60-70% of reps | < 50% (calibration problem) |
**Magic Number:** Net New ARR × 4 ÷ Prior Quarter S&M Spend
**CAC Payback:** S&M Spend ÷ New Logo ARR × (1 / Gross Margin %)
### Revenue Waterfall
```
Opening ARR
+ New Logo ARR
+ Expansion ARR (upsell, cross-sell, seat adds)
- Contraction ARR (downgrades)
- Churned ARR
= Closing ARR
NRR = (Opening + Expansion - Contraction - Churn) / Opening
```
### NRR Benchmarks
| NRR | Signal |
|-----|--------|
| > 120% | World-class. Grow even with zero new logos. |
| 100-120% | Healthy. Existing base is growing. |
| 90-100% | Concerning. Churn eating growth. |
| < 90% | Crisis. Fix before scaling sales. |
## Red Flags
- NRR declining two quarters in a row — customer value story is broken
- Pipeline coverage below 3x entering the quarter — already forecasting a miss
- Win rate dropping while sales cycle extends — competitive pressure or ICP drift
- < 50% of sales team quota-attaining — comp plan, ramp, or quota calibration issue
- Average deal size declining — moving downmarket under pressure (dangerous)
- Magic Number below 0.5 — sales spend not converting to revenue
- Forecast accuracy below 80% — reps sandbagging or pipeline quality is poor
- Single customer > 15% of ARR — concentration risk, board will flag this
- "Too expensive" appearing in > 40% of loss notes — value demonstration broken, not pricing
- Expansion ARR < 20% of total ARR — upsell motion isn't working
## Integration with Other C-Suite Roles
| When... | CRO works with... | To... |
|---------|------------------|-------|
| Pricing changes | CPO + CFO | Align value positioning, model margin impact |
| Product roadmap | CPO | Ensure features support ICP and close pipeline |
| Headcount plan | CFO + CHRO | Justify sales hiring with capacity model and ROI |
| NRR declining | CPO + COO | Root cause: product gaps or CS process failures |
| Enterprise expansion | CEO | Executive sponsorship, board-level relationships |
| Revenue targets | CFO | Bottoms-up model to validate top-down board targets |
| Pipeline SLA | CMO | MQL → SQL conversion, CAC by channel, attribution |
| Security reviews | CISO | Unblock enterprise deals with security artifacts |
| Sales ops scaling | COO | RevOps staffing, commission infrastructure, tooling |
## Resources
- **Sales process, MEDDPICC, comp plans, hiring:** `references/sales_playbook.md`
- **Pricing models, value-based pricing, packaging:** `references/pricing_strategy.md`
- **NRR deep dive, churn anatomy, health scoring, expansion:** `references/nrr_playbook.md`
- **Revenue forecast model (CLI):** `scripts/revenue_forecast_model.py`
- **Churn & retention analyzer (CLI):** `scripts/churn_analyzer.py`
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- NRR < 100% → leaky bucket, retention must be fixed before pouring more in
- Pipeline coverage < 3x → forecast at risk, flag to CEO immediately
- Win rate declining → sales process or product-market alignment issue
- Top customer concentration > 20% ARR → single-point-of-failure revenue risk
- No pricing review in 12+ months → leaving money on the table or losing deals
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Forecast next quarter" | Pipeline-based forecast with confidence intervals |
| "Analyze our churn" | Cohort churn analysis with at-risk accounts and intervention plan |
| "Review our pricing" | Pricing analysis with competitive benchmarks and recommendations |
| "Scale the sales team" | Capacity model with quota, ramp, territories, comp plan |
| "Revenue board section" | ARR waterfall, NRR, pipeline, forecast, risks |
## Reasoning Technique: Chain of Thought
Pipeline math must be explicit: leads → MQLs → SQLs → opportunities → closed. Show conversion rates at each stage. Question any assumption above historical averages.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:references/nrr_playbook.md
# NRR Playbook
Net Revenue Retention is the single most important metric for a SaaS company's health and valuation. A company with 120% NRR grows even if it closes zero new deals. A company with 80% NRR is filling a bucket with a hole in it.
---
## NRR Deep Dive
### The Fundamental Formula
```
NRR = (Opening MRR + Expansion MRR - Contraction MRR - Churned MRR) / Opening MRR
Example:
Opening MRR: $1,000,000
Expansion: +$150,000
Contraction: -$30,000
Churn: -$80,000
Closing MRR: $1,040,000
NRR = $1,040,000 / $1,000,000 = 104%
```
### NRR vs. GRR
| Metric | Formula | What It Tells You |
|--------|---------|------------------|
| **GRR** | (Opening - Contraction - Churn) / Opening | Retention floor — how much you keep without any expansion |
| **NRR** | (Opening + Expansion - Contraction - Churn) / Opening | Net health — expansion offsetting churn |
| **Logo Retention** | (Customers start - Customers churned) / Customers start | Volume retention, ignores revenue weight |
**GRR is the floor. NRR is the ceiling.**
If GRR is 80% and NRR is 105%, your expansion is covering 25 points of churn. That's fragile — any expansion slowdown turns NRR negative. The fix is GRR, not more upsell.
### Benchmarks by Segment
| Segment | Good GRR | Good NRR | Exceptional NRR |
|---------|---------|---------|----------------|
| SMB-focused | 80-85% | 95-105% | > 110% |
| Mid-Market | 85-90% | 105-115% | > 120% |
| Enterprise | 90-95% | 115-130% | > 140% |
Enterprise NRR can exceed 140% because large accounts expand substantially and rarely churn entirely — they may downgrade but full logo churn is rare if the product is embedded.
### NRR by Cohort
Don't just measure NRR across the full base — measure it by customer cohort (month of acquisition).
```
Jan 2024 Cohort:
Opening MRR (Jan 2024): $50,000
MRR at Jan 2025: $62,000
12-month NRR: 124%
Feb 2024 Cohort:
Opening MRR (Feb 2024): $45,000
MRR at Feb 2025: $38,000
12-month NRR: 84% ← problem cohort
```
Cohort analysis reveals:
- Whether a specific acquisition channel brings lower-quality customers
- Whether a product change or pricing shift affected retention
- Whether specific sales reps or time periods created bad-fit deals
---
## Churn Anatomy
Not all churn is equal. Know the breakdown before prescribing solutions.
### Churn Types
| Type | Definition | Primary Cause | Fix |
|------|-----------|--------------|-----|
| **Logo churn** | Customer cancels entirely | No value, poor fit, champion left, competitor | Root cause analysis, ICP tightening |
| **Revenue churn** | ARR lost (cancels + downgrades combined) | Same as logo + downgrade triggers | Address both volume and revenue |
| **Involuntary churn** | Failed payment, expired card | Billing friction | Dunning improvement (quick win: 20-30% recovery) |
| **Voluntary churn** | Active cancellation decision | Explicit dissatisfaction, competitor win | Exit interview + intervention program |
| **Contraction** | Downgrade, seat reduction | Overpurchased, budget cut, team reduction | Right-sizing program, annual contracts |
### Churn Root Cause Framework
Run this analysis quarterly on all churned accounts:
**Step 1: Categorize by reason**
- No value realized (never activated or adopted)
- Value realized but budget cut (external, not product)
- Switched to competitor (why? what did they offer?)
- Champion left company (relationship loss, not product failure)
- Company shutdown / acquisition (unavoidable)
**Step 2: Look for patterns**
- Which ICP signals predict churn? (company size, vertical, acquisition channel)
- Which product behaviors predict churn? (no login in 30 days, never completed onboarding)
- Which time periods have highest churn? (months 3, 6, 12 are typical cliff points)
**Step 3: Act on the patterns**
- ICP pattern → tighten qualification criteria
- Behavior pattern → build early warning health score
- Time cliff → build intervention playbooks for months 2, 5, 11
### Exit Interview Protocol
Talk to every churned customer if ACV > $10K. For smaller, do quarterly batch surveys.
Questions:
1. "What was the primary reason for your decision to cancel?"
2. "What would have needed to be true for you to stay?"
3. "What did you switch to, and what drove that decision?"
4. "Was there a specific moment when you decided to leave?"
Rules:
- CSM who owned the account should NOT conduct the exit interview (too much relationship bias)
- Use a neutral party or the VP CS
- Document verbatim, not paraphrased
- Feed patterns back to Product and Sales monthly
---
## Customer Health Scoring
A health score predicts churn 60-90 days before it happens. Without one, you're reactive.
### Health Score Components
Score each account 0-100 across weighted signals:
| Signal | Weight | Red (0-33) | Yellow (34-66) | Green (67-100) |
|--------|--------|-----------|---------------|---------------|
| **Product usage** (DAU/WAU, feature adoption depth) | 35% | < 20% seats active | 20-60% seats active | > 60% seats active |
| **Engagement** (QBR attendance, champion responsiveness) | 20% | No response 60+ days | 30-60 days | Active, < 30 days |
| **NPS / CSAT** | 20% | Score < 6 | Score 6-7 | Score 8-10 |
| **Support volume** (negative signal: high volume = friction) | 15% | > 10 tickets/month | 3-10/month | < 3/month |
| **Contract signals** (time to renewal, expansion in motion) | 10% | < 60 days to renewal, no expansion discussion | 60-90 days, passive | > 90 days, expansion active |
**Composite score:**
- 70-100: Healthy. Renewal confident. Identify expansion opportunity.
- 50-69: At-risk. CSM check-in required. Executive sponsor loop-in if < 60 days to renewal.
- 0-49: Red alert. Immediate intervention. VP CS or CEO call if strategic account.
### Health Score Automation
Trigger alerts automatically:
```
Score drops > 20 points in 30 days → CSM immediate outreach (same day)
No product login in 14 days → Automated email + CSM flag (within 24 hours)
Champion leaves company → Executive outreach (within 24 hours)
Support escalation → CSM loop-in (within 2 hours)
Renewal < 90 days + score < 60 → VP CS review (weekly)
Seat utilization < 30% → Adoption intervention playbook triggered
```
### Leading Indicators vs. Lagging Indicators
| Leading (predict future churn) | Lagging (confirm past churn) |
|-------------------------------|------------------------------|
| Login frequency declining | Cancellation submitted |
| Feature adoption stalling at basic level | Non-renewal at contract end |
| NPS score trend (not just snapshot) | Downgrade executed |
| No QBR scheduled in 90+ days | Champion departure |
| Support escalations increasing | Competitor mentioned in support |
Build your health score from leading indicators. Lagging indicators tell you what already happened.
---
## Expansion Revenue Strategies
Expansion is cheaper than acquisition. CAC for expansion is typically 20-30% of new logo CAC.
### Expansion Motion 1: Seat Expansion
**Trigger signals:**
- Usage by unlicensed users (shared logins, "can you add my colleague?")
- Team growth visible on LinkedIn (company hiring in target department)
- Champion promotes to a new role with bigger team
- Power users at license limit consistently
**Playbook:**
1. Pull monthly usage report showing which features unlicensed users are using
2. Frame as: "Your team is getting value from X — you could be capturing that for the full team"
3. Offer a team expansion proposal at renewal + 10% volume discount for seat adds
4. Never penalize users for sharing logins before the conversation — that's a data asset
### Expansion Motion 2: Upsell (Tier Upgrade)
**Trigger signals:**
- Customer consistently hitting usage/feature limits
- Security or compliance requirement that requires higher tier
- New stakeholder joining who needs admin controls
- API usage growing rapidly (engineering team engagement)
**Playbook:**
1. Build a "value realized" report before the upsell conversation (ROI proof)
2. Use QBR as the venue: "You've achieved X. Here's what's possible at the next level."
3. Frame the upgrade as unlocking more of what's already working
4. Time to renewal: start upsell conversation 90-120 days before renewal
### Expansion Motion 3: Cross-sell
**Trigger signals:**
- Strategic account with adjacent problem your product can solve
- New product launch that complements existing usage
- Customer explicitly asks about a capability in your roadmap or adjacent product
**Playbook:**
1. Land with core product; build relationship and prove value
2. Cross-sell only after health score is green and NPS > 7
3. Introduce the new product through a champion, not a cold pitch
4. Pilot pricing: bundle into renewal at modest uplift vs. separate sale
5. Cross-sell owner: CSM or AE (define explicitly — joint ownership = no ownership)
### Expansion Sequencing
Don't try all three simultaneously. Sequence matters:
```
Month 0-3: Activation focus — ensure core value delivered
Month 3-6: Seat expansion — grow usage within existing team
Month 6-9: Upsell conversation — unlock advanced features
Month 9-12: Cross-sell OR renewal + multi-year lock-in
```
### NRR Modeling
Target breakdown for 115% NRR:
```
GRR: 88% (12% lost to churn/contraction)
Expansion rate: 27% (upsell + cross-sell + seat expansion)
NRR: 88% + 27% = 115%
To reach 120% NRR:
Option A: Improve GRR to 92% (reduce churn), keep expansion at 28%
Option B: Keep GRR at 88%, improve expansion to 32%
Option C: Both, incrementally
Option A is usually easier and more durable. Fix the hole first.
```
---
## Customer Success Integration
CS and Revenue are not separate functions. NRR lives at their intersection.
### CS Team Structure (aligned to NRR)
| CS Model | When to Use | NRR Focus |
|----------|------------|-----------|
| **High-touch CSM** | ACV > $25K | Named accounts, QBRs, executive relationships |
| **Tech-touch / pooled** | ACV $5K-25K | Automated health scoring, office hours, community |
| **Self-serve** | ACV < $5K | In-app guidance, knowledge base, email sequences |
**CSM coverage ratios:**
- High-touch: 1 CSM per $2M-4M ARR managed
- Tech-touch: 1 CSM per $5M-10M ARR managed
- Self-serve: Product and automation (no dedicated CSM)
### CS Compensation (aligned to NRR)
Don't pay CSMs a flat salary — align incentive to retention and expansion:
```
CS compensation structure:
Base: 70% of OTE
Variable: 30% of OTE
Variable tied to:
GRR / NRR vs. target (50% of variable)
Health score improvement (25% of variable)
Expansion ARR facilitated (25% of variable)
Do NOT pay CS commission on expansion ARR the same way AEs earn it.
This creates conflict: CS will push expansion before the customer is ready.
Instead, bonus for expansion milestones — it's a different incentive structure.
```
### QBR (Quarterly Business Review) Framework
QBRs are the primary vehicle for expansion and churn prevention in enterprise accounts.
**QBR agenda (60-90 minutes):**
1. **Their goals, our progress** — review what they said success looked like at kickoff (10 min)
2. **Usage and adoption data** — product metrics presented in business language, not feature language (15 min)
3. **Value delivered** — ROI proof: time saved, revenue influenced, risk reduced (10 min)
4. **Challenges and blockers** — what's preventing more adoption? (10 min)
5. **Roadmap preview** — upcoming features relevant to their use case (10 min)
6. **Next 90 days** — joint success plan with owner and due dates (10 min)
7. **Expansion opportunity** — if health score is green and timing is right (10 min)
**QBR anti-patterns:**
- Leading with your product roadmap (they don't care; start with their results)
- Bringing too many people from your side without matching seniority
- Presenting at a VP without bringing the economic buyer
- Skipping QBRs for "healthy" accounts (health can change fast)
- No confirmed next step at the end
---
## Cohort-Based Retention Analysis
Aggregate NRR hides the signal. Cohort analysis reveals it.
### Retention Curve Analysis
Plot retention by months since acquisition for each quarterly cohort:
```
Month 0: 100% (starting revenue)
Month 3: First cliff — early adopters who didn't activate churn here
Month 6: Second cliff — customers who never expanded, running out of runway
Month 12: Renewal cliff — annual contract renewal decision
Month 18: Mature customers — churn rate stabilizes significantly
Healthy curve: Drops sharply in months 1-3, flattens after month 6
Problem curve: Continues declining linearly through month 12+ (no value anchor)
```
### Reading Cohort Data
| Pattern | Interpretation | Action |
|---------|---------------|--------|
| Early churn (months 1-3) | Onboarding / activation failure | Fix time-to-value, improve onboarding |
| Mid-cycle churn (months 4-8) | Value not deepening | Adoption program, check product fit |
| Annual renewal churn (month 12) | Buying committee didn't renew | Executive engagement, earlier renewal process |
| Flat after month 6 | Sticky product, low expansion | Increase upsell motion |
| Growing after month 6 | Expansion working | Scale the upsell playbook |
### Cohort Segmentation Variables
Slice retention cohorts by:
- **Acquisition channel** (inbound vs. outbound vs. PLG vs. partner)
- **Sales rep** (which reps close durable deals vs. churny deals)
- **Deal size** (SMB churn rate typically 2-3x enterprise)
- **Industry vertical** (some verticals have structurally higher churn)
- **Product tier at signup** (self-serve → converted vs. directly contracted)
- **Geographic market** (international markets often have different retention profiles)
The most actionable finding is usually by acquisition channel or sales rep — both are directly controllable.
### Churn Prevention Intervention Playbooks
**Playbook 1: Low Activation (no login in first 14 days)**
```
Day 7: Automated email: "Getting started" + specific next step
Day 14: CSM outreach: "I noticed you haven't logged in — can I help?"
Day 21: Escalate to CSM manager if no response
Day 30: Executive outreach for ACV > $25K; flag as at-risk
```
**Playbook 2: Usage Cliff (DAU drops > 50% in 30 days)**
```
Trigger: Automated health score alert
Day 1: CSM reviews usage report, identifies likely cause
Day 2: CSM outreach: "We noticed your team's usage changed — is everything okay?"
Day 7: If no response: schedule 30-min call with champion
Day 14: If unresponsive: VP CS loop-in + executive reach out
```
**Playbook 3: Champion Departure**
```
Trigger: LinkedIn alert or internal report of champion leaving
Day 1: Email to departed champion (warm handoff ask)
Day 1: Email to new stakeholder (introduction from AE or VP CS)
Day 3: Schedule onboarding call for new stakeholder
Day 14: QBR with new stakeholder to establish relationship
Day 30: Health score review — flag if engagement hasn't recovered
```
**Playbook 4: Pre-Renewal (90 days out, health score < 70)**
```
Day -90: CSM completes account health review, escalates if < 70
Day -75: Executive sponsor from vendor side joins renewal call
Day -60: Value delivered report prepared (ROI proof)
Day -45: Renewal proposal sent with expansion option
Day -30: Follow-up on any open objections or requirements
Day -14: Final confirm or escalate to VP Sales
```
FILE:references/pricing_strategy.md
# Pricing Strategy
Pricing is not a one-time decision. It's an ongoing hypothesis about value and willingness to pay. Most SaaS companies are underpriced by 20-40%.
---
## Pricing Models
### Per Seat / User
**How it works:** Customer pays a fixed amount per user, per month or year.
**Best for:**
- Collaboration tools (everyone who uses it needs a license)
- Productivity software where value scales with users
- Products where you want viral / network growth within accounts
**Pricing structure:**
```
Starter: $15/user/month (1-10 users)
Professional: $30/user/month (11-100 users)
Enterprise: Custom (100+ users, negotiated)
```
**Pros:**
- Simple to understand and sell
- Revenue scales naturally with customer growth
- Predictable for customers (fixed monthly cost)
**Cons:**
- Customers negotiate volume discounts aggressively
- Discourages broad adoption if price is high (seat hoarding)
- Doesn't capture value for power users vs. light users
- Enterprises can negotiate $5/seat on a $25 product
**Watch for:** Customers sharing logins to avoid per-seat cost. Enforce with IP restrictions or SSO audit logs.
---
### Usage-Based Pricing (UBP)
**How it works:** Customer pays for what they consume — API calls, data processed, messages sent, compute hours, etc.
**Best for:**
- API companies, infrastructure, data platforms
- AI products (per-token, per-query pricing)
- Products where value scales non-linearly with usage
- Land-and-expand: low entry cost, grows with customer success
**Pricing structure:**
```
Free tier: First 10K API calls/month
Pay-as-you-go: $0.002 per API call
Committed use: $500/month for 500K calls (better rate)
Enterprise: Custom contract, committed volume discount
```
**Pros:**
- Customer pays in proportion to value received
- Low barrier to entry (customers start small, scale up)
- Natural expansion: customer success = revenue growth
- No "unused licenses" problem
**Cons:**
- Revenue is unpredictable for both you and the customer
- Hard to forecast; hard to budget for customer
- Customers may optimize to reduce usage (and your revenue)
- Complex billing; requires robust usage tracking infrastructure
**Usage-based pricing math:**
```
Unit cost (your COGS per unit): $0.0002 per API call
Target gross margin: 80%
Price = COGS / (1 - margin) = $0.0002 / 0.20 = $0.001 minimum
Add markup for value delivered above cost: $0.002 per call (10x markup at scale)
```
**Hybrid usage + seat approach:**
- Platform fee: $500/month (access, support, base features)
- Usage fee: $0.001 per API call above included 100K
---
### Flat Rate / Subscription
**How it works:** One price for full access, regardless of usage or users.
**Best for:**
- Simple products with limited feature differentiation
- Products where usage is predictable and bounded
- Customers who want budget certainty
- Early stage before you've figured out value segmentation
**Pros:**
- Simplest to sell and explain
- Easiest billing implementation
- Customers love budget predictability
**Cons:**
- Leaves money on the table for heavy users
- No natural expansion revenue mechanism
- Light users pay the same as power users (retention risk)
**When to move away from flat rate:**
- 20% of customers are using 80% of the product capacity
- Power users would clearly pay more; light users churn or underutilize
- You have a clear expansion story waiting to happen
---
### Tiered / Feature-Based
**How it works:** Multiple packages (Starter, Pro, Enterprise) with different feature sets and/or usage limits.
**Best for:**
- Multi-use-case products
- Different buyer types (individual vs. team vs. enterprise)
- Products with a natural upgrade path based on sophistication
**Structure (Good / Better / Best):**
```
Starter ($49/mo): Core features, 3 users, 10GB storage
Professional ($149/mo): Advanced features, 25 users, 100GB, API access
Business ($499/mo): All features, 100 users, 1TB, SSO, priority support
Enterprise (custom): Unlimited, custom integrations, SLA, dedicated CSM
```
**Tier design principles:**
- Starter tier: removes friction, proves value, not the revenue center
- Professional: the primary revenue tier; 60-70% of customers land here
- Enterprise: custom pricing allows you to capture maximum value
- Each tier upgrade should have an obvious "must-have" feature for the target buyer
**What to gate on each tier:**
| Feature Type | Where to Put It |
|-------------|----------------|
| Core product functionality | Starter (must be useful) |
| Collaboration features | Pro (drives team usage) |
| Admin, security, SSO | Business/Enterprise |
| API / integrations | Pro and above |
| SLAs, dedicated support | Enterprise only |
| Advanced analytics | Business/Enterprise |
---
### Hybrid Pricing
**How it works:** Combination of models (e.g., platform fee + per seat + usage).
**Example:**
```
Platform fee: $2,000/month (access, core features, admin console)
Per seat: $50/user/month (up to 200 users)
Usage overage: $0.10/action above 100K included actions
```
**When to use hybrid:**
- Enterprise customers want budget certainty (platform fee) but your value scales with usage
- You have different cost structures for different features
- Customers have very different usage patterns across the base
**Pros:** Captures value at multiple dimensions. Hybrid is most common in enterprise SaaS.
**Cons:** More complex to explain and bill. Sales training burden increases.
---
## Value-Based Pricing Methodology
Cost-plus pricing is a race to the bottom. Price on value, not cost.
### Step 1: Define the Economic Outcome
What business result does your product deliver? Be specific.
**Weak:** "We help companies save time"
**Strong:** "We reduce onboarding time for new enterprise software by 40%, saving 8 hours per employee"
Map to one of:
- **Revenue increase** — "Our customers close 25% more deals using our CRM intelligence"
- **Cost reduction** — "We eliminate 60% of manual data entry for finance teams"
- **Risk reduction** — "We reduce compliance violations by 90%, avoiding $500K+ in potential fines"
- **Time savings** — "CSMs spend 5 fewer hours per week on manual reporting"
### Step 2: Quantify Per Customer
Calculate the dollar value of the outcome for your average customer.
```
Example: Data entry automation product
Target customer: 50-person finance team
Manual data entry: 4 hours/person/week
Hours saved with product: 2.4 hours/person/week (60% reduction)
Fully loaded cost of finance analyst: $75/hour
Weekly savings: 50 employees × 2.4 hours × $75 = $9,000
Annual savings: $9,000 × 52 weeks = $468,000
```
### Step 3: Determine Willingness to Pay
Customers will typically pay 10-20% of the value delivered for software.
```
Annual value delivered: $468,000
Willingness to pay range: $46,800 - $93,600/year
Current market pricing: ~$60,000/year
Your pricing: $72,000/year (between median and upper WTP)
```
**Test your hypothesis:**
- Interview 5-10 customers: "If we charged $X/year, is that reasonable?"
- Van Westendorp Price Sensitivity Meter:
- "At what price is this too cheap to trust?"
- "At what price is this a good deal?"
- "At what price is this getting expensive but still worth it?"
- "At what price is this too expensive?"
### Step 4: Validate with Win Rate Analysis
```
Run this analysis quarterly:
Track win rate by price point (segmented if possible)
Win rate 30-40%: pricing is likely right
Win rate < 20%: price is too high OR value demonstration is broken
Win rate > 50%: you're underpriced
Note: Distinguish between "lost on price" and "lost on fit."
Lost on price + good ROI proof: test lower price or improve value story
Lost on fit: ICP problem, not pricing problem
```
---
## Packaging (Good / Better / Best)
### The Three-Package Framework
Packaging is not just about features. It's about serving different buyer personas with different budgets and needs.
**Buyer personas by tier:**
```
Starter → The individual contributor or small team trying to solve an immediate problem
- Low budget authority
- Low-friction purchase (credit card, self-serve)
- Needs quick time to value
Professional → The team manager or department head
- $10K-100K budget authority
- Works with inside sales
- Needs collaboration features and reporting
Enterprise → The VP or C-suite buyer
- Unlimited budget (but requires justification)
- Needs compliance, security, SLAs, dedicated support
- Long buying process, multiple stakeholders
```
### Packaging Design Rules
1. **Each tier must be useful on its own.** Starter can't be crippled—customers need to succeed.
2. **Upgrade triggers must be obvious.** When a customer hits a limit, the next tier should solve it clearly.
3. **Don't gate features that drive adoption.** Collaboration features gated in a low tier kill viral growth.
4. **Enterprise pricing is custom.** Show "Contact Sales" or a starting price. Don't publish a firm enterprise price—you'll anchor too low.
5. **Annual vs. monthly pricing:** Charge 15-25% more for monthly vs. annual. Incentivize annual prepay.
### Pricing Page Design
- Lead with the most popular tier (visually prominent)
- Show annual pricing by default (with toggle to monthly)
- Highlight one or two "recommended" plans
- Feature comparison table: minimize the number of rows (overwhelm = no decision)
- Show logos of customers on each tier (social proof by segment)
- Live chat for enterprise CTA, not "Contact Sales" form
---
## Pricing Experiments and Rollout
### Before You Change Pricing
**Internal checklist:**
- [ ] Validate new pricing with 5-10 current customers (interviews)
- [ ] Run a willingness-to-pay survey with 50+ prospects
- [ ] Model revenue impact: how many customers at new pricing are equivalent to current ARR?
- [ ] Get CFO sign-off on cash flow impact
- [ ] Prepare messaging for customers, website, sales team
- [ ] Set a rollout date 60-90 days out
### Testing Approaches
**Cohort testing (safest):**
- New signups see new pricing; existing customers are grandfathered
- Monitor: conversion rate, ACV, win rate, time-to-close
- Run for 90 days before full rollout
**A/B pricing test (higher stakes):**
- Half of new signups see price A, half see price B
- Risk: word gets out that prices differ (customer frustration)
- Use only on self-serve, where purchase is not sales-assisted
**Segment-specific rollout:**
- Change pricing in one segment (e.g., SMB) while holding enterprise steady
- Lower risk than full rollout; validate before expanding
### Pricing Rollout Plan
```
Day 0: Decision made, pricing document approved
Day -60: Internal communication to sales, CS, support
Day -45: Customer communication drafted and reviewed
Day -30: New pricing live on website for new customers
Day -30: Existing customer email sent (90-day grandfather period)
Day -30: Sales team trained, FAQ document ready
Day -14: Second reminder to existing customers
Day 0: Existing customers transition to new pricing
Day +30: Win rate analysis, NRR impact review
```
### Grandfathering Policy
- **Standard:** Grandfather existing customers at old price for 12 months
- **Aggressive:** 90 days grandfather, then new pricing applies (use if you're raising significantly)
- **Never:** Retroactive pricing changes with no notice. This is a churn trigger and brand damage.
Grandfathering message framing:
> "We're investing significantly in [feature areas]. As a valued customer, your pricing remains unchanged through [date]. After that, your new rate will be $X — still X% less than new customer pricing as a thank-you for your partnership."
---
## Competitive Pricing Analysis
### Mapping the Competitive Landscape
```
Step 1: List all direct competitors
Step 2: Find their public pricing (website, G2, Capterra)
Step 3: Secret shop their sales process for unpublished pricing
Step 4: Talk to customers who considered them ("What did they quote you?")
Step 5: Map to your packaging (apples-to-apples comparison)
Output: Competitive pricing matrix
You: $X/month per seat at Pro tier
Competitor A: $Y/month per seat at equivalent tier
Competitor B: Custom (enterprise only)
```
### Competitive Positioning by Price
| Your Position | Situation | Response |
|--------------|-----------|---------|
| Significantly cheaper | Unclear why | Raise prices or clarify differentiation |
| Slightly cheaper | Winning on price | Test raising price, monitor win rate |
| At market | Competing on features | Make sure differentiation is clear in sales |
| Slightly more expensive | Win rate healthy | Price is justified by value |
| Significantly more expensive | Win rate low | Improve value proof or re-examine ICP |
### When "They're Cheaper" Appears in Deals
**Coach your reps:**
1. "What makes [Competitor] worth choosing over the $X difference?" (reframe value, not price)
2. "If price were equal, which would you choose and why?" (understand true preference)
3. "What's the cost of not solving this problem in Q3?" (urgency + value)
4. "What's their implementation cost and time?" (TCO, not ACV)
**If price is truly the barrier:**
- Offer a pilot at reduced scope (not price) to prove value
- Multi-year deal with year-one discount
- Defer payment to match their budget cycle (start in Q4, bill in Q1)
- Confirm it's price and not a champion issue or lack of urgency
---
## When to Raise Prices
### Green Lights for a Price Increase
**Product signals:**
- Customer usage growing QoQ (product delivers real value)
- NPS consistently > 40
- Feature requests indicate you're solving critical workflows
- Customers measuring and can articulate ROI
**Market signals:**
- Win rate > 35% (strong signal of underpricing)
- Waitlist or high inbound conversion without price objections
- Competitors raising prices (market is moving up)
- You've added significant value (new features, integrations, uptime improvements)
**Business signals:**
- Gross margin below 70% (cost inflation requires pricing response)
- CAC payback > 24 months (need higher ACV to fix unit economics)
- Haven't raised prices in 2+ years (inflation alone justifies adjustment)
### How Much to Raise
**Conservative:** 10-15% increase. Low risk, low disruption.
**Standard:** 15-30% increase. Acceptable if value story is strong.
**Aggressive:** 30-50% increase. Only with major product investment or clear underprice.
**Repositioning:** 2-5x increase. Rare; requires moving to a new buyer persona.
**Rule:** If fewer than 20% of prospects mention price as a concern, you're underpriced. Test.
### Price Increase Execution
1. Raise new business pricing immediately on the website
2. Communicate to existing customers with 90 days notice
3. Grandfather for 12 months OR give a 10-15% loyalty discount on new price
4. Track: conversion rate (new business), churn rate (existing), expansion ARR impact
5. Monitor win rate for 60 days post-increase; adjust if win rate drops > 5 points
**What not to do:**
- Don't apologize for raising prices
- Don't over-explain the justification (confident framing wins)
- Don't let sales reps negotiate discounts back to old pricing "just this once"
- Don't raise prices and remove features simultaneously
FILE:references/sales_playbook.md
# Sales Playbook
Frameworks for building, running, and scaling a B2B SaaS sales organization.
---
## Sales Process Design
A sales process is a repeatable series of steps that takes a prospect from first contact to closed revenue. Without it, you have individual heroics, not a scalable machine.
### The Core Funnel
```
Lead Generation → Qualification → Discovery → Demo → Trial / POC → Proposal → Negotiation → Close → Handoff
```
Each stage has a clear entry criterion, exit criterion, and owner.
### Stage Definitions
#### Stage 0: Lead / Suspect
- **Entry:** Contact exists in CRM with basic firmographic data
- **Owner:** Marketing or SDR
- **Exit criterion:** Meets ICP criteria (company size, industry, tech stack)
- **Action:** Research, prioritize, add to outbound sequence
#### Stage 1: Prospecting / Outreach
- **Entry:** ICP-qualified account, no contact yet
- **Owner:** SDR or AE (depending on model)
- **Exit criterion:** Meeting booked with a qualified contact
- **Action:** Multi-channel outreach (email + call + LinkedIn), 8-12 touch sequence
- **Key metric:** Meeting booked rate (benchmark: 2-5% of outbound contacts)
#### Stage 2: Discovery
- **Entry:** First meeting confirmed
- **Owner:** AE (SDR hands off or joins)
- **Exit criterion:** Confirmed: pain, budget range, decision process, timeline
- **Action:** Ask questions. Listen. Map the org. Don't pitch yet.
- **Key metric:** Discovery-to-demo rate (benchmark: 60-80% proceed)
**Discovery question framework:**
```
Situation: "How do you currently handle [problem area]?"
Problem: "What's the impact when [pain point] happens?"
Implication: "If this continues, what does that mean for [business goal]?"
Need-payoff: "If we solved this, what would that be worth to you?"
```
#### Stage 3: Demo / Solution Presentation
- **Entry:** Confirmed pain and fit from discovery
- **Owner:** AE (+ SE for complex products)
- **Exit criterion:** Prospect agrees to evaluate / trial; next step defined
- **Action:** Show the workflow that solves their specific pain (not a feature tour)
- **Key metric:** Demo-to-trial/proposal rate (benchmark: 40-60%)
**Demo structure:**
1. Recap their pain (show you listened) — 5 min
2. Show the "aha moment" (fastest path to value) — 10 min
3. Walk the specific workflow they described — 15 min
4. Handle objections, confirm fit — 5 min
5. Define clear next step (date, owners, criteria) — 5 min
Never show features they didn't ask for. Every additional feature is noise until they have a reason to care.
#### Stage 4: Trial / POC
- **Entry:** Prospect commits to evaluate with real data/use case
- **Owner:** AE + CSM or SE
- **Exit criterion:** Success criteria met, POC success confirmed
- **Action:** Define success criteria upfront (in writing). Set a tight timeframe (2-4 weeks max).
- **Key metric:** POC-to-proposal rate (benchmark: 50-70%)
**POC setup requirements:**
```
Before any POC:
□ Signed NDA
□ Written success criteria ("We'll move forward if X happens")
□ Named champion who owns the evaluation
□ Executive sponsor identified
□ Defined timeline with end date
□ Agreed next step if criteria are met
```
If you can't get written success criteria, you don't have a real opportunity. You have a "we'll see."
#### Stage 5: Proposal / Pricing
- **Entry:** POC success OR strong discovery fit for simple products
- **Owner:** AE
- **Exit criterion:** Proposal received, timeline to decision confirmed
- **Action:** Present in a live call, never email a proposal cold
- **Key metric:** Proposal-to-negotiation rate (benchmark: 50-75%)
**Proposal structure:**
1. Problem statement (their words, not yours)
2. Proposed solution (mapped to their workflow)
3. ROI summary (value delivered vs. investment)
4. Pricing options (give 2-3 options; anchors the decision)
5. Next steps with dates
#### Stage 6: Negotiation
- **Entry:** Verbal intent to proceed, price/terms discussion begins
- **Owner:** AE (+ VP Sales for large deals)
- **Exit criterion:** Mutual agreement on terms; contract sent
- **Action:** Never discount before they ask. Discount on scope, not on margin.
- **Key metric:** Negotiation win rate (benchmark: 70-85%)
**Negotiation principles:**
- Get something for everything you give. Discount → multi-year. Fast close → early pay discount.
- Don't negotiate against yourself. Silence after an offer is not rejection.
- Know your walk-away before you enter. If you don't have a BATNA, you have no leverage.
- Legal/procurement delay ≠ deal death. Keep the champion engaged.
#### Stage 7: Close
- **Entry:** Signed contract or PO received
- **Owner:** AE
- **Exit criterion:** Contract countersigned, kickoff date set
- **Action:** Celebrate with the customer. Immediately introduce CSM.
- **Key metric:** Average close rate (closed won ÷ all closed = won + lost)
#### Stage 8: Handoff to Customer Success
- **Entry:** Deal closed
- **Owner:** AE + CSM
- **Exit criterion:** Customer has met their assigned CSM, kickoff scheduled
- **Action:** Internal handoff call with AE + CSM. AE shares: deal context, key stakeholders, use case, success criteria, any promises made during the sale.
**Handoff document (AE fills before first CS meeting):**
```
Account: [name]
ACV: $X
Close date: [date]
Primary contact: [name, title, email]
Economic buyer: [name, title]
Use case: [specific workflow]
Success criteria: [what they said good looks like in 90 days]
Promises made: [anything specific committed during sale]
Risk flags: [competitive, budget, champion strength]
```
---
## MEDDPICC Qualification Framework
MEDDPICC is the enterprise qualification standard. If you can't answer every letter, you don't have a qualified opportunity — you have a conversation.
### M — Metrics
What is the quantified business impact? What does winning look like in numbers?
- "What's the current cost of [the problem]?"
- "How do you measure success in this area today?"
- "If we achieve X outcome, what does that save or earn you?"
**Red flag:** No metrics = no business case = hard to get budget.
### E — Economic Buyer
Who has final authority to approve the budget?
- "Who else will be involved in the final decision?"
- "Have you purchased solutions in this range before? Who approved that?"
- "When we get to final terms, who needs to sign?"
**Red flag:** You only know the user buyer. Economic buyer hasn't engaged.
### D — Decision Criteria
What factors will they use to evaluate and select a solution?
- "What's most important in your evaluation?"
- "How will you compare options?"
- "What does the ideal solution look like to you?"
**Why it matters:** If you don't know their criteria, you're guessing what to prove. Define the criteria before you compete on them.
### D — Decision Process
What are the steps from evaluation to signed contract?
- "Walk me through your process from here to signed agreement."
- "Does procurement get involved? Legal? InfoSec?"
- "Have you purchased software at this price before? How long did that take?"
**Red flag:** No defined process = unlimited sales cycle.
### P — Paper Process
What's the contract and legal process?
- "Who manages vendor contracts on your side?"
- "What's your standard MSA, or do you use ours?"
- "How long does legal review typically take?"
**Why it matters:** Legal and procurement have killed many "done" deals. Start early. Route to your legal team simultaneously.
### I — Identify Pain
What is the specific, felt pain driving this evaluation?
- "What triggered this initiative now vs. six months ago?"
- "What happens if you don't solve this in Q3?"
- "On a scale of 1-10, how urgent is this for your team?"
**Red flag:** Pain isn't felt by the economic buyer. User pain ≠ budget authority.
### C — Champion
Who will actively sell your solution internally when you're not in the room?
- "Who else have you brought into this evaluation?"
- "Can you help us get access to [economic buyer / IT / security]?"
- "If the decision went the wrong way, who would be disappointed?"
**Red flag:** Your champion is enthusiastic but has no internal influence.
### C — Competition
Who else are they evaluating? What's your position?
- "Are you looking at alternatives?"
- "What made you start with us?"
- "Have you used [Competitor X] before?"
**Why it matters:** Knowing the competitive field tells you what you need to prove and what to neutralize.
### MEDDPICC Scorecard
| Letter | Score 1 | Score 2 | Score 3 |
|--------|---------|---------|---------|
| Metrics | No numbers | Approximate value | Specific ROI model |
| Economic Buyer | Unknown | Named, not engaged | Engaged directly |
| Decision Criteria | Vague | Partially defined | Written, weighted |
| Decision Process | Unknown | Verbal description | Steps confirmed, timeline known |
| Paper Process | Unknown | Basic awareness | Legal contacts, standard process known |
| Identify Pain | No urgency | User-level pain | Executive-level pain with consequences |
| Champion | No advocate | Friendly contact | Actively selling internally |
| Competition | Unknown | Identified | Position mapped, differentiation clear |
**Score each 1-3. Total 16+/24 = qualified opportunity. Under 12 = unqualified, do not forecast.**
---
## Sales Compensation Plans
Comp drives behavior. Design it precisely.
### Base / Variable Split
| Role | Base % | Variable % | Rationale |
|------|--------|-----------|-----------|
| SDR | 60-70% | 30-40% | Activity-based, not purely revenue |
| AE (Inside Sales) | 50% | 50% | Balanced risk/reward |
| AE (Enterprise) | 55-60% | 40-45% | Longer cycle, higher base for stability |
| VP Sales | 50% | 50% | Accountable for team results |
| CSM (retention focus) | 70% | 30% | Less variable, stable relationship role |
| CSM (expansion focus) | 60% | 40% | Expansion quota adds variable |
### Commission Structure
**Standard AE plan:**
```
Base: $80K
Variable: $80K (at 100% quota attainment)
OTE: $160K
Commission rate: OTE variable ÷ Quota
If quota = $800K ARR: commission = $80K ÷ $800K = 10% of ARR closed
Accelerators (performance above quota):
101-125% quota: 1.25x commission rate (12.5% of ARR)
126-150% quota: 1.5x commission rate (15% of ARR)
> 150% quota: 2.0x commission rate (20% of ARR)
```
**Why accelerators matter:**
- They keep top performers motivated past quota
- They make it possible for top reps to earn $200K+ (attracting talent)
- They create the "make it rain" culture
### SDR Compensation
SDRs are measured on output (meetings booked, pipeline created), not closed revenue.
```
Quota: 20 qualified meetings booked per month (or $X pipeline created)
Commission: $150-300 per qualified meeting held
Accelerators:
If a meeting converts to closed won: Bonus $250-500
If monthly meetings > 125% of quota: 1.5x rate on upside meetings
```
### Clawbacks
A clawback recovers commission paid on deals that churn or are fraudulently closed.
**Common clawback rules:**
- Full clawback if customer cancels within 90 days of close
- 50% clawback if customer cancels within 91-180 days
- No clawback after 180 days (AE shouldn't be penalized for future CS failures)
- Clawbacks vest: pay commission immediately but apply against next quarter's payout if triggered
**Why clawbacks matter:**
- Without them, reps are incentivized to close any deal, regardless of fit
- With them, reps self-qualify more carefully
### SPIFFs (Sales Performance Incentive Funds)
Short-term tactical incentives for specific behaviors:
- $5K bonus for closing a new vertical deal this quarter
- 1.5x commission on annual prepay deals in Q4
- $1K for closing a deal in a new geographic territory
Use SPIFFs sparingly. Overuse trains reps to wait for the SPIFF before engaging.
### Multi-Year and Prepay Incentives
Align rep behavior with company cash flow:
- Multi-year deals: Credit full TCV against quota, pay commission upfront on TCV
- Annual prepay: 10-20% uplift on commission rate
- Monthly billing: Standard commission rate
---
## Enterprise vs. SMB vs. Self-Serve Models
### Self-Serve / PLG
**Characteristics:**
- Product is the primary acquisition channel
- Credit card required (no invoicing)
- No human touch in the initial purchase
- Sales engages only at enterprise signals (high usage, team expansion, compliance needs)
**Funnel:**
```
Website → Free trial / Freemium → Activation → PQL → Expansion → Enterprise
```
**Key metrics:**
- Free-to-paid conversion rate (benchmark: 2-5% of signups)
- Time to activation (first core action)
- PQL → expansion conversion rate
- NRR from self-serve base
**Sales involvement triggers (PQL signals):**
- Team size > 10 seats
- Usage spikes (power user patterns)
- Feature limit hits on core features
- Job title change (new economic buyer appears in account)
### SMB Inside Sales
**Characteristics:**
- ACV $5K-25K
- 30-60 day sales cycle
- Inbound-heavy or light outbound
- SDR → AE → CS model
- Phone + email + video; no in-person
**Funnel:**
```
Inbound/MQL → SDR qualifies → AE discovery → Demo → Proposal → Close
```
**Key metrics:**
- MQL-to-SQL rate (benchmark: 15-25%)
- SQL-to-close rate (benchmark: 20-30%)
- Average sales cycle (30-60 days)
- AE productivity: $600K-$1M quota per rep
**Team ratios:**
- 1 SDR supports 3-4 AEs
- 1 CSM manages $1M-2M ARR
### Enterprise Sales
**Characteristics:**
- ACV $50K+
- 90-365 day sales cycle
- Outbound prospecting + inbound from brand
- AE + SE + executive sponsor model
- Multi-stakeholder: champion, economic buyer, IT, legal, procurement
**Funnel:**
```
Account targeting → Executive outreach → Discovery → POC → Security review → Legal → Procurement → Close
```
**Key metrics:**
- Deals in pipeline (volume matters less, quality more)
- POC win rate (benchmark: 60-75%)
- Average sales cycle (3-12 months)
- AE productivity: $1.5M-$3M quota per rep
**Team ratios:**
- 1 SE supports 3-4 AEs
- 1 CSM manages $2M-5M ARR (named accounts, high-touch)
---
## Sales Hiring and Ramp
### What "Good" Looks Like by Role
**SDR (entry level):**
- 1-2 years of outbound experience OR strong track record in customer-facing role
- Resilient: rejection is the job
- Coachable: SDR is a proving ground, not a final destination
- Can write clear, concise prospecting emails without templates
**AE (inside sales):**
- 2-4 years sales experience, preferably SaaS
- Can articulate their process for a discovery call
- Knows their numbers: quota, attainment, average deal size, sales cycle
- Shows how they build pipeline (AEs who only work inbound are a risk)
**AE (enterprise):**
- 4-8 years B2B sales, at least 2 in enterprise
- Has closed deals > $100K ACV
- Can name the stakeholders in a complex deal they navigated
- Understands procurement, security review, multi-year contracts
**VP Sales:**
- Has scaled a team from where you are to 2x your size
- Can build a comp plan from scratch
- Has hiring and firing experience
- Revenue from a repeatable process, not personal relationships
### Interview Process
**3-stage process:**
1. **Recruiter screen** (30 min): Motivation, experience, logistics
2. **Manager interview** (60 min): Structured questions on process, examples, numbers
3. **Panel / role play** (90 min): Mock discovery call + debrief; team fit
**Role play rubric:**
- Did they prepare (knew your product, your ICP)?
- Did they ask before pitching?
- Did they handle pushback without capitulating immediately?
- Did they confirm a next step with a date?
### Onboarding Structure (6-Week Ramp)
| Week | Focus | Activities |
|------|-------|-----------|
| 1 | Company, product, ICP | Onboarding sessions, product sandbox, shadow AE calls |
| 2 | Sales process, tools, messaging | CRM training, call review, write first prospecting emails |
| 3 | First outreach | Send first sequences, book first meetings, shadow closes |
| 4 | Independent discovery | Lead own discovery calls with manager reviewing |
| 5 | Full cycle | Handle pipeline independently, weekly coaching |
| 6 | Quota-bearing | 25% of quota expectation; full accountability begins |
### Performance Management
**Clear standards, no surprises:**
```
Month 3: 25% of quota expected. Miss by > 50% → performance conversation.
Month 4: 50% of quota expected. Miss by > 40% → PIP warning.
Month 5: 75% of quota. Miss by > 30% → formal PIP.
Month 6+: 100% of quota. Consistent miss → exit.
```
**PIP (Performance Improvement Plan) — not for show:**
- Should include specific, measurable targets (not "improve attitude")
- 30-60 day timeline
- Weekly check-ins with manager
- If targets aren't met: exit, no extensions
- A PIP that doesn't lead to improvement or exit is a management failure
**Rule:** Low performers who stay cost you your top performers. They watch what you tolerate.
FILE:scripts/churn_analyzer.py
#!/usr/bin/env python3
"""
Churn & Retention Analyzer
===========================
Customer-level churn and Net Revenue Retention (NRR) analysis for B2B SaaS.
Calculates:
- Gross Revenue Retention (GRR) and Net Revenue Retention (NRR)
- Monthly and annual churn rates (logo + revenue)
- Cohort-based retention curves
- At-risk account identification
- Expansion revenue segmentation
- ARR waterfall (new / expansion / contraction / churn)
Usage:
python churn_analyzer.py
python churn_analyzer.py --csv customers.csv
python churn_analyzer.py --period 2026-Q1 --output summary
Input format (CSV):
customer_id, name, segment, arr, start_date, [churn_date], [expansion_arr], [contraction_arr]
Stdlib only. No dependencies.
"""
import csv
import sys
import json
import argparse
import statistics
from datetime import date, datetime, timedelta
from collections import defaultdict
from io import StringIO
from itertools import groupby
# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------
class Customer:
def __init__(self, customer_id, name, segment, arr, start_date,
churn_date=None, expansion_arr=0.0, contraction_arr=0.0,
health_score=None):
self.customer_id = customer_id
self.name = name
self.segment = segment
self.arr = float(arr)
self.start_date = self._parse_date(start_date)
self.churn_date = self._parse_date(churn_date) if churn_date else None
self.expansion_arr = float(expansion_arr or 0)
self.contraction_arr = float(contraction_arr or 0)
self.health_score = float(health_score) if health_score else None
@staticmethod
def _parse_date(value):
if not value or str(value).strip() in ("", "None", "null"):
return None
for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y", "%Y/%m/%d"):
try:
return datetime.strptime(str(value).strip(), fmt).date()
except ValueError:
continue
raise ValueError(f"Cannot parse date: {value!r}")
def is_churned(self):
return self.churn_date is not None
def is_active(self, as_of=None):
as_of = as_of or date.today()
if self.churn_date and self.churn_date <= as_of:
return False
return self.start_date <= as_of
def tenure_days(self, as_of=None):
as_of = as_of or date.today()
end = self.churn_date if self.churn_date else as_of
return (end - self.start_date).days
def tenure_months(self, as_of=None):
return self.tenure_days(as_of) / 30.44
def cohort_month(self):
"""Acquisition cohort: YYYY-MM of start_date."""
return self.start_date.strftime("%Y-%m")
def cohort_quarter(self):
q = (self.start_date.month - 1) // 3 + 1
return f"Q{q} {self.start_date.year}"
def net_arr(self):
"""Current ARR + expansion - contraction."""
return self.arr + self.expansion_arr - self.contraction_arr
def days_since_acquisition(self, as_of=None):
as_of = as_of or date.today()
return (as_of - self.start_date).days
# ---------------------------------------------------------------------------
# Core metrics
# ---------------------------------------------------------------------------
class RetentionAnalyzer:
def __init__(self, customers, as_of=None):
self.customers = customers
self.as_of = as_of or date.today()
def active_customers(self, as_of=None):
as_of = as_of or self.as_of
return [c for c in self.customers if c.is_active(as_of)]
def churned_customers(self, start=None, end=None):
"""Customers who churned in [start, end]."""
result = []
for c in self.customers:
if not c.churn_date:
continue
if start and c.churn_date < start:
continue
if end and c.churn_date > end:
continue
result.append(c)
return result
def arr_waterfall(self, period_start, period_end):
"""
Calculate ARR waterfall for a given period.
Returns dict with opening_arr, new_arr, expansion_arr, contraction_arr,
churned_arr, closing_arr, nrr, grr.
"""
# Opening: active at period start
opening_customers = [c for c in self.customers if c.is_active(period_start)]
opening_arr = sum(c.arr for c in opening_customers)
opening_ids = {c.customer_id for c in opening_customers}
# New: started during the period
new_customers = [
c for c in self.customers
if period_start < c.start_date <= period_end
]
new_arr = sum(c.arr for c in new_customers)
# Churned: were active at start, churn_date within period
churned = [
c for c in opening_customers
if c.churn_date and period_start < c.churn_date <= period_end
]
churned_arr = sum(c.arr for c in churned)
# Expansion and contraction: from customers active at opening
expansion = sum(
c.expansion_arr for c in opening_customers
if not c.is_churned() or (c.churn_date and c.churn_date > period_end)
)
contraction = sum(
c.contraction_arr for c in opening_customers
if not c.is_churned() or (c.churn_date and c.churn_date > period_end)
)
closing_arr = opening_arr + new_arr + expansion - contraction - churned_arr
grr = (opening_arr - contraction - churned_arr) / opening_arr if opening_arr else 0
nrr = (opening_arr + expansion - contraction - churned_arr) / opening_arr if opening_arr else 0
return {
"period_start": period_start.isoformat(),
"period_end": period_end.isoformat(),
"opening_arr": opening_arr,
"new_arr": new_arr,
"expansion_arr": expansion,
"contraction_arr": contraction,
"churned_arr": churned_arr,
"closing_arr": closing_arr,
"net_new_arr": new_arr + expansion - contraction - churned_arr,
"grr": max(0.0, grr),
"nrr": max(0.0, nrr),
}
def logo_churn_rate(self, period_start, period_end):
"""Logo churn rate for a period."""
opening = [c for c in self.customers if c.is_active(period_start)]
churned = [
c for c in opening
if c.churn_date and period_start < c.churn_date <= period_end
]
return len(churned) / len(opening) if opening else 0.0
def revenue_churn_rate(self, period_start, period_end):
"""Gross revenue churn rate for a period."""
opening = [c for c in self.customers if c.is_active(period_start)]
opening_arr = sum(c.arr for c in opening)
churned_arr = sum(
c.arr for c in opening
if c.churn_date and period_start < c.churn_date <= period_end
)
contraction = sum(c.contraction_arr for c in opening)
return (churned_arr + contraction) / opening_arr if opening_arr else 0.0
# ---------------------------------------------------------------------------
# Cohort analysis
# ---------------------------------------------------------------------------
class CohortAnalyzer:
def __init__(self, customers):
self.customers = customers
def build_cohorts(self):
"""Group customers by acquisition cohort (month)."""
cohorts = defaultdict(list)
for c in self.customers:
cohorts[c.cohort_month()].append(c)
return dict(sorted(cohorts.items()))
def retention_at_month(self, cohort_customers, months_after):
"""
What fraction of cohort ARR remains `months_after` months after acquisition?
"""
if not cohort_customers:
return None
opening_arr = sum(c.arr for c in cohort_customers)
if opening_arr == 0:
return None
earliest_start = min(c.start_date for c in cohort_customers)
check_date = earliest_start + timedelta(days=int(months_after * 30.44))
if check_date > date.today():
return None # Future — no data
retained_arr = sum(
c.arr for c in cohort_customers
if c.is_active(check_date)
)
return retained_arr / opening_arr
def retention_curve(self, cohort_customers, max_months=24):
"""Return retention at months 0, 3, 6, 9, 12, 18, 24."""
checkpoints = [0, 3, 6, 9, 12, 18, 24]
checkpoints = [m for m in checkpoints if m <= max_months]
curve = {}
for m in checkpoints:
rate = self.retention_at_month(cohort_customers, m)
if rate is not None:
curve[m] = rate
return curve
def cohort_report(self):
"""Returns dict: cohort → {size, opening_arr, retention_curve}."""
cohorts = self.build_cohorts()
report = {}
for cohort_month, customers in cohorts.items():
curve = self.retention_curve(customers)
report[cohort_month] = {
"customer_count": len(customers),
"opening_arr": sum(c.arr for c in customers),
"churned_count": sum(1 for c in customers if c.is_churned()),
"current_retention": curve.get(12, curve.get(max(curve.keys()) if curve else 0)),
"retention_curve": curve,
}
return report
def identify_at_risk(self, tenure_months_max=6, health_threshold=60):
"""
Identify at-risk customers based on:
- Low health score (if available)
- Short tenure (haven't proved long-term value)
- High contraction signals
"""
at_risk = []
for c in self.customers:
if c.is_churned():
continue
reasons = []
score = 0
# Health score signal
if c.health_score is not None and c.health_score < health_threshold:
reasons.append(f"Health score {c.health_score:.0f} < {health_threshold}")
score += 40
# Early tenure risk
tenure = c.tenure_months()
if tenure < tenure_months_max:
reasons.append(f"Tenure {tenure:.1f} months (< {tenure_months_max})")
score += 20
# Contraction signal
if c.contraction_arr > 0:
contraction_pct = c.contraction_arr / c.arr
reasons.append(f"Contraction {contraction_pct:.0%} of ARR")
score += 30
# No expansion in mature account
if tenure > 12 and c.expansion_arr == 0:
reasons.append("No expansion after 12+ months (stagnant)")
score += 10
if score > 0:
at_risk.append({
"customer_id": c.customer_id,
"name": c.name,
"segment": c.segment,
"arr": c.arr,
"tenure_months": round(tenure, 1),
"health_score": c.health_score,
"risk_score": score,
"risk_reasons": reasons,
})
return sorted(at_risk, key=lambda x: -x["risk_score"])
# ---------------------------------------------------------------------------
# Expansion analysis
# ---------------------------------------------------------------------------
class ExpansionAnalyzer:
def __init__(self, customers):
self.customers = customers
def expansion_summary(self):
active = [c for c in self.customers if not c.is_churned()]
expanding = [c for c in active if c.expansion_arr > 0]
contracting = [c for c in active if c.contraction_arr > 0]
total_arr = sum(c.arr for c in active)
total_expansion = sum(c.expansion_arr for c in active)
total_contraction = sum(c.contraction_arr for c in active)
return {
"active_customers": len(active),
"total_arr": total_arr,
"expanding_count": len(expanding),
"contracting_count": len(contracting),
"expansion_arr": total_expansion,
"contraction_arr": total_contraction,
"expansion_rate": total_expansion / total_arr if total_arr else 0,
"contraction_rate": total_contraction / total_arr if total_arr else 0,
"net_expansion_rate": (total_expansion - total_contraction) / total_arr if total_arr else 0,
}
def expansion_by_segment(self):
active = [c for c in self.customers if not c.is_churned()]
by_segment = defaultdict(lambda: {"arr": 0.0, "expansion": 0.0,
"contraction": 0.0, "count": 0})
for c in active:
seg = c.segment or "Unspecified"
by_segment[seg]["arr"] += c.arr
by_segment[seg]["expansion"] += c.expansion_arr
by_segment[seg]["contraction"] += c.contraction_arr
by_segment[seg]["count"] += 1
result = {}
for seg, data in by_segment.items():
arr = data["arr"]
result[seg] = {
"customer_count": data["count"],
"arr": arr,
"expansion_arr": data["expansion"],
"contraction_arr": data["contraction"],
"expansion_rate": data["expansion"] / arr if arr else 0,
"net_nrr_contribution": (arr + data["expansion"] - data["contraction"]) / arr if arr else 0,
}
return result
def top_expansion_candidates(self, min_tenure_months=6, min_arr=5000):
"""
Customers who are active, healthy tenure, but have zero expansion.
These are upsell/expansion targets.
"""
active = [c for c in self.customers if not c.is_churned()]
candidates = []
for c in active:
tenure = c.tenure_months()
if (tenure >= min_tenure_months
and c.arr >= min_arr
and c.expansion_arr == 0
and (c.health_score is None or c.health_score >= 60)):
candidates.append({
"customer_id": c.customer_id,
"name": c.name,
"segment": c.segment,
"arr": c.arr,
"tenure_months": round(tenure, 1),
"health_score": c.health_score,
})
return sorted(candidates, key=lambda x: -x["arr"])
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt_currency(value):
if value >= 1_000_000:
return f".2fM"
if value >= 1_000:
return f".1fK"
return f".0f"
def fmt_pct(value):
return f"{value * 100:.1f}%"
def nrr_status(nrr):
if nrr >= 1.20:
return "✅ World-class"
if nrr >= 1.10:
return "✅ Healthy"
if nrr >= 1.00:
return "⚠️ Acceptable"
if nrr >= 0.90:
return "🔴 Concerning"
return "🔴 Crisis"
def grr_status(grr):
if grr >= 0.90:
return "✅ Strong"
if grr >= 0.85:
return "⚠️ Acceptable"
return "🔴 Below threshold"
def print_header(title):
width = 70
print()
print("=" * width)
print(f" {title}")
print("=" * width)
def print_section(title):
print(f"\n--- {title} ---")
def print_full_report(customers, period_start, period_end):
analyzer = RetentionAnalyzer(customers, as_of=period_end)
cohort_analyzer = CohortAnalyzer(customers)
expansion_analyzer = ExpansionAnalyzer(customers)
print_header("CHURN & RETENTION ANALYZER")
print(f" Analysis period: {period_start.isoformat()} → {period_end.isoformat()}")
print(f" Total customers in dataset: {len(customers)}")
active = analyzer.active_customers(period_end)
churned_in_period = analyzer.churned_customers(period_start, period_end)
print(f" Active at period end: {len(active)}")
print(f" Churned in period: {len(churned_in_period)}")
# ── ARR Waterfall
print_section("ARR WATERFALL")
wf = analyzer.arr_waterfall(period_start, period_end)
print(f" Opening ARR: {fmt_currency(wf['opening_arr'])}")
print(f" + New Logo ARR: +{fmt_currency(wf['new_arr'])}")
print(f" + Expansion ARR: +{fmt_currency(wf['expansion_arr'])}")
print(f" - Contraction ARR: -{fmt_currency(wf['contraction_arr'])}")
print(f" - Churned ARR: -{fmt_currency(wf['churned_arr'])}")
print(f" {'─'*42}")
print(f" Closing ARR: {fmt_currency(wf['closing_arr'])}")
print(f" Net New ARR: {'+' if wf['net_new_arr'] >= 0 else ''}{fmt_currency(wf['net_new_arr'])}")
# ── NRR / GRR
print_section("RETENTION METRICS")
nrr = wf["nrr"]
grr = wf["grr"]
logo_churn = analyzer.logo_churn_rate(period_start, period_end)
rev_churn = analyzer.revenue_churn_rate(period_start, period_end)
print(f" NRR (Net Revenue Retention): {fmt_pct(nrr)} {nrr_status(nrr)}")
print(f" GRR (Gross Revenue Retention): {fmt_pct(grr)} {grr_status(grr)}")
print(f" Logo Churn Rate (period): {fmt_pct(logo_churn)}")
print(f" Revenue Churn Rate (period): {fmt_pct(rev_churn)}")
if wf["opening_arr"] > 0:
expansion_rate = wf["expansion_arr"] / wf["opening_arr"]
print(f" Expansion Rate (period): {fmt_pct(expansion_rate)}")
print()
print(f" NRR Benchmark: >120% world-class | 100-120% healthy | <100% fix immediately")
# ── Expansion summary
print_section("EXPANSION REVENUE")
exp = expansion_analyzer.expansion_summary()
print(f" Expanding customers: {exp['expanding_count']} / {exp['active_customers']} ({fmt_pct(exp['expanding_count']/exp['active_customers']) if exp['active_customers'] else '—'})")
print(f" Contracting: {exp['contracting_count']} / {exp['active_customers']}")
print(f" Expansion ARR: {fmt_currency(exp['expansion_arr'])} ({fmt_pct(exp['expansion_rate'])} of base)")
print(f" Contraction ARR: {fmt_currency(exp['contraction_arr'])}")
print(f" Net Expansion Rate: {fmt_pct(exp['net_expansion_rate'])}")
# ── Segment breakdown
print_section("SEGMENT BREAKDOWN (NRR Components)")
seg_data = expansion_analyzer.expansion_by_segment()
col_w = [18, 8, 12, 10, 10, 10]
h = (f" {'Segment':<{col_w[0]}} {'Custs':>{col_w[1]}} {'ARR':>{col_w[2]}} "
f"{'Expansion':>{col_w[3]}} {'Contraction':>{col_w[4]}} {'NRR':>{col_w[5]}}")
print(h)
print(" " + "-" * (sum(col_w) + 5))
for seg, data in sorted(seg_data.items(), key=lambda x: -x[1]["arr"]):
print(f" {seg:<{col_w[0]}} {data['customer_count']:>{col_w[1]}} "
f"{fmt_currency(data['arr']):>{col_w[2]}} "
f"{fmt_currency(data['expansion_arr']):>{col_w[3]}} "
f"{fmt_currency(data['contraction_arr']):>{col_w[4]}} "
f"{fmt_pct(data['net_nrr_contribution']):>{col_w[5]}}")
# ── Cohort retention
print_section("COHORT RETENTION CURVES")
cohort_report = cohort_analyzer.cohort_report()
print(f" {'Cohort':<10} {'Custs':>6} {'Opening ARR':>13} {'Mo.3':>8} {'Mo.6':>8} {'Mo.12':>8}")
print(" " + "-" * 57)
for cohort, data in cohort_report.items():
curve = data["retention_curve"]
m3 = fmt_pct(curve[3]) if 3 in curve else " —"
m6 = fmt_pct(curve[6]) if 6 in curve else " —"
m12 = fmt_pct(curve[12]) if 12 in curve else " —"
print(f" {cohort:<10} {data['customer_count']:>6} "
f"{fmt_currency(data['opening_arr']):>13} "
f"{m3:>8} {m6:>8} {m12:>8}")
# ── At-risk accounts
print_section("AT-RISK ACCOUNTS")
at_risk = cohort_analyzer.identify_at_risk()
if at_risk:
print(f" {'Customer':<22} {'Segment':<14} {'ARR':>10} {'Tenure':>8} {'Risk':>6} Reason")
print(" " + "-" * 80)
for acct in at_risk[:10]: # Top 10
reason_short = acct["risk_reasons"][0] if acct["risk_reasons"] else ""
tenure_str = f"{acct['tenure_months']}mo"
print(f" {acct['name']:<22} {acct['segment']:<14} "
f"{fmt_currency(acct['arr']):>10} {tenure_str:>8} "
f"{acct['risk_score']:>5} {reason_short}")
if len(at_risk) > 10:
print(f" ... and {len(at_risk) - 10} more at-risk accounts")
else:
print(" ✅ No at-risk accounts identified")
# ── Expansion candidates
print_section("EXPANSION CANDIDATES (no expansion yet, healthy tenure)")
candidates = expansion_analyzer.top_expansion_candidates()
if candidates:
print(f" {'Customer':<22} {'Segment':<14} {'ARR':>10} {'Tenure':>8} Action")
print(" " + "-" * 70)
for c in candidates[:8]:
action = "Upsell review" if c["arr"] > 20000 else "Seat expansion call"
tenure_str = f"{c['tenure_months']}mo"
print(f" {c['name']:<22} {c['segment']:<14} "
f"{fmt_currency(c['arr']):>10} {tenure_str:>8} {action}")
else:
print(" ✅ All eligible accounts have expansion in motion")
# ── Red flags
print_section("HEALTH FLAGS")
flags = []
if nrr < 1.0:
flags.append("🔴 NRR below 100% — revenue base is shrinking. Fix before scaling sales.")
if grr < 0.85:
flags.append(f"🔴 GRR {fmt_pct(grr)} — gross retention below 85% threshold. Churn is a product/CS problem.")
if logo_churn > 0.05:
flags.append(f"⚠️ Logo churn {fmt_pct(logo_churn)} this period — run cohort analysis to find the pattern.")
if exp["expansion_rate"] < 0.10 and exp["active_customers"] > 10:
flags.append("⚠️ Expansion rate below 10% — upsell motion is weak or non-existent.")
churned_arr_pct = wf["churned_arr"] / wf["opening_arr"] if wf["opening_arr"] else 0
if churned_arr_pct > 0.10:
flags.append(f"🔴 Revenue churn at {fmt_pct(churned_arr_pct)} of opening ARR this period — high urgency.")
if len(at_risk) > len(active) * 0.20:
flags.append(f"⚠️ {len(at_risk)} of {len(active)} active accounts flagged at-risk ({fmt_pct(len(at_risk)/len(active) if active else 0)})")
if flags:
for f in flags:
print(f" {f}")
else:
print(" ✅ No critical health flags")
print()
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
SAMPLE_CSV = """customer_id,name,segment,arr,start_date,churn_date,expansion_arr,contraction_arr,health_score
C001,Acme Manufacturing,Enterprise,120000,2023-01-15,,45000,0,82
C002,TechStart Inc,Mid-Market,28000,2023-02-01,,8000,0,74
C003,Global Retail Co,Enterprise,250000,2023-01-05,,0,25000,45
C004,MedTech Solutions,Mid-Market,45000,2023-03-10,,15000,0,88
C005,FinServ Holdings,Enterprise,185000,2023-01-20,2023-09-15,0,0,
C006,StartupHub Network,SMB,12000,2023-04-01,,0,3000,55
C007,EduPlatform Inc,Mid-Market,32000,2023-02-15,,10000,0,91
C008,BioLab Analytics,Enterprise,95000,2023-01-10,,20000,0,78
C009,RegionalBank Corp,Enterprise,310000,2023-03-01,,75000,0,85
C010,CloudOps Systems,Mid-Market,38000,2023-05-01,2024-01-10,0,0,
C011,InsurTech Platform,Mid-Market,55000,2023-06-15,,0,0,62
C012,LegalAI Corp,SMB,18000,2023-07-01,,5000,0,79
C013,RetailChain Ltd,Enterprise,140000,2023-04-20,,0,20000,41
C014,DataPipeline Co,Mid-Market,42000,2023-08-01,,12000,0,83
C015,NanoTech Startup,SMB,9500,2023-09-15,2024-02-28,0,0,
C016,MedDevice Corp,Enterprise,220000,2023-02-28,,60000,0,92
C017,ConsultingFirm XYZ,SMB,15000,2023-10-01,,0,5000,38
C018,GovTech Solutions,Enterprise,175000,2023-11-15,,0,0,71
C019,AgriData Systems,Mid-Market,31000,2024-01-10,,8000,0,77
C020,HealthcarePlus,Mid-Market,62000,2024-02-01,,0,0,65
"""
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def load_customers_from_csv(csv_text):
reader = csv.DictReader(StringIO(csv_text))
customers = []
errors = []
for i, row in enumerate(reader, start=2):
try:
c = Customer(
customer_id=row.get("customer_id", f"row_{i}"),
name=row.get("name", f"Customer {i}"),
segment=row.get("segment", ""),
arr=row.get("arr", 0),
start_date=row.get("start_date", ""),
churn_date=row.get("churn_date", None) or None,
expansion_arr=row.get("expansion_arr", 0) or 0,
contraction_arr=row.get("contraction_arr", 0) or 0,
health_score=row.get("health_score", None) or None,
)
customers.append(c)
except (ValueError, KeyError) as e:
errors.append(f" Row {i}: {e}")
if errors:
print("⚠️ Skipped rows with errors:")
for err in errors:
print(err)
return customers
def parse_period(period_str):
"""Parse 'YYYY-QN' or 'YYYY-MM' into (start_date, end_date)."""
if not period_str:
today = date.today()
q = (today.month - 1) // 3
start = date(today.year, q * 3 + 1, 1)
# End of current quarter
end_month = start.month + 2
end_year = start.year + (end_month - 1) // 12
end_month = ((end_month - 1) % 12) + 1
import calendar
end_day = calendar.monthrange(end_year, end_month)[1]
return start, date(end_year, end_month, end_day)
import calendar
if "-Q" in period_str:
year, qpart = period_str.split("-Q")
year = int(year)
q = int(qpart)
start_month = (q - 1) * 3 + 1
end_month = start_month + 2
start = date(year, start_month, 1)
end = date(year, end_month, calendar.monthrange(year, end_month)[1])
return start, end
# YYYY-MM
year, month = period_str.split("-")
year, month = int(year), int(month)
start = date(year, month, 1)
end = date(year, month, calendar.monthrange(year, month)[1])
return start, end
def main():
parser = argparse.ArgumentParser(
description="Churn & Retention Analyzer — NRR, cohort analysis, at-risk detection"
)
parser.add_argument(
"--csv", metavar="FILE",
help="CSV file with customer data (uses sample data if not provided)"
)
parser.add_argument(
"--period", metavar="PERIOD",
help='Analysis period: "2026-Q1" or "2026-03" (defaults to current quarter)'
)
parser.add_argument(
"--output", choices=["summary", "full", "json"],
default="full",
help="Output format (default: full)"
)
args = parser.parse_args()
# Load data
if args.csv:
try:
with open(args.csv, "r", encoding="utf-8") as f:
csv_text = f.read()
except FileNotFoundError:
print(f"Error: File not found: {args.csv}", file=sys.stderr)
sys.exit(1)
else:
print("No --csv provided. Using sample customer data.\n")
csv_text = SAMPLE_CSV
customers = load_customers_from_csv(csv_text)
if not customers:
print("No customers loaded. Exiting.", file=sys.stderr)
sys.exit(1)
period_start, period_end = parse_period(args.period)
if args.output == "json":
analyzer = RetentionAnalyzer(customers, as_of=period_end)
cohort_analyzer = CohortAnalyzer(customers)
expansion_analyzer = ExpansionAnalyzer(customers)
wf = analyzer.arr_waterfall(period_start, period_end)
output = {
"period": {"start": period_start.isoformat(), "end": period_end.isoformat()},
"arr_waterfall": wf,
"logo_churn_rate": analyzer.logo_churn_rate(period_start, period_end),
"revenue_churn_rate": analyzer.revenue_churn_rate(period_start, period_end),
"cohort_report": {k: {**v, "retention_curve": {str(m): r for m, r in v["retention_curve"].items()}}
for k, v in cohort_analyzer.cohort_report().items()},
"at_risk_accounts": cohort_analyzer.identify_at_risk(),
"expansion_summary": expansion_analyzer.expansion_summary(),
"expansion_by_segment": expansion_analyzer.expansion_by_segment(),
"expansion_candidates": expansion_analyzer.top_expansion_candidates(),
}
print(json.dumps(output, indent=2))
elif args.output == "summary":
analyzer = RetentionAnalyzer(customers, as_of=period_end)
wf = analyzer.arr_waterfall(period_start, period_end)
print_header("NRR SUMMARY")
print(f" Period: {period_start.isoformat()} → {period_end.isoformat()}")
print(f" NRR: {fmt_pct(wf['nrr'])} {nrr_status(wf['nrr'])}")
print(f" GRR: {fmt_pct(wf['grr'])} {grr_status(wf['grr'])}")
print(f" Opening: {fmt_currency(wf['opening_arr'])}")
print(f" Closing: {fmt_currency(wf['closing_arr'])}")
print(f" Net New: {fmt_currency(wf['net_new_arr'])}")
print()
else:
print_full_report(customers, period_start, period_end)
if __name__ == "__main__":
main()
FILE:scripts/revenue_forecast_model.py
#!/usr/bin/env python3
"""
Revenue Forecast Model
======================
Pipeline-based revenue forecasting for B2B SaaS.
Models:
- Weighted pipeline (stage probability × deal value)
- Historical win rate adjustment (calibrate to actuals)
- Scenario analysis (conservative / base / upside)
- Monthly and quarterly projection with confidence ranges
Usage:
python revenue_forecast_model.py
python revenue_forecast_model.py --csv pipeline.csv
python revenue_forecast_model.py --scenario conservative
Input format (CSV):
deal_id, name, stage, arr_value, close_date, rep, segment
Stdlib only. No dependencies.
"""
import csv
import sys
import json
import argparse
import statistics
from datetime import date, datetime, timedelta
from collections import defaultdict
from io import StringIO
# ---------------------------------------------------------------------------
# Stage configuration
# ---------------------------------------------------------------------------
DEFAULT_STAGE_PROBABILITIES = {
"discovery": 0.10,
"qualification": 0.25,
"demo": 0.40,
"proposal": 0.55,
"poc": 0.65,
"negotiation": 0.80,
"verbal_commit": 0.92,
"closed_won": 1.00,
"closed_lost": 0.00,
}
SCENARIO_MULTIPLIERS = {
"conservative": 0.85, # Win rate 15% below historical
"base": 1.00, # Historical win rate
"upside": 1.15, # Win rate 15% above historical
}
# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------
class Deal:
def __init__(self, deal_id, name, stage, arr_value, close_date, rep="", segment=""):
self.deal_id = deal_id
self.name = name
self.stage = stage.lower().replace(" ", "_").replace("/", "_")
self.arr_value = float(arr_value)
self.close_date = self._parse_date(close_date)
self.rep = rep
self.segment = segment
@staticmethod
def _parse_date(value):
for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y", "%Y/%m/%d"):
try:
return datetime.strptime(str(value), fmt).date()
except ValueError:
continue
raise ValueError(f"Cannot parse date: {value!r}")
@property
def quarter(self):
q = (self.close_date.month - 1) // 3 + 1
return f"Q{q} {self.close_date.year}"
@property
def month_key(self):
return self.close_date.strftime("%Y-%m")
def weighted_value(self, stage_probs, scenario="base"):
prob = stage_probs.get(self.stage, 0.0)
multiplier = SCENARIO_MULTIPLIERS.get(scenario, 1.0)
# Clamp probability to [0, 1]
adjusted = min(1.0, max(0.0, prob * multiplier))
return self.arr_value * adjusted
def is_open(self):
return self.stage not in ("closed_won", "closed_lost")
def is_closed_won(self):
return self.stage == "closed_won"
# ---------------------------------------------------------------------------
# Win rate calibration
# ---------------------------------------------------------------------------
def calculate_historical_win_rates(deals):
"""
Calculate actual win rates per stage from closed deals.
Returns a dict: stage → win_rate (float).
Requires deals that were at each stage and are now closed won/lost.
"""
# In a real implementation, you'd have historical stage-at-point-in-time data.
# Here we approximate: among closed deals, what fraction were won?
closed = [d for d in deals if not d.is_open()]
if not closed:
return {}
won = [d for d in closed if d.is_closed_won()]
overall_rate = len(won) / len(closed) if closed else 0.0
# Stage-level calibration: adjust default probs by actual overall rate
# (In production: use CRM historical stage-level conversion data)
calibrated = {}
for stage, default_prob in DEFAULT_STAGE_PROBABILITIES.items():
if overall_rate > 0:
calibrated[stage] = min(1.0, default_prob * (overall_rate / 0.25))
else:
calibrated[stage] = default_prob
return calibrated
# ---------------------------------------------------------------------------
# Forecast engine
# ---------------------------------------------------------------------------
class ForecastEngine:
def __init__(self, deals, stage_probs=None):
self.deals = deals
self.stage_probs = stage_probs or DEFAULT_STAGE_PROBABILITIES
def open_deals(self):
return [d for d in self.deals if d.is_open()]
def closed_won_deals(self):
return [d for d in self.deals if d.is_closed_won()]
def pipeline_by_month(self, scenario="base"):
"""Returns dict: month_key → weighted ARR."""
result = defaultdict(float)
for deal in self.open_deals():
result[deal.month_key] += deal.weighted_value(self.stage_probs, scenario)
return dict(sorted(result.items()))
def pipeline_by_quarter(self, scenario="base"):
"""Returns dict: quarter → weighted ARR."""
result = defaultdict(float)
for deal in self.open_deals():
result[deal.quarter] += deal.weighted_value(self.stage_probs, scenario)
return dict(sorted(result.items()))
def coverage_ratio(self, quota, period_filter=None):
"""
Pipeline coverage = total pipeline ÷ quota.
period_filter: if set, only include deals with close_date in that period.
"""
pipeline = sum(
d.arr_value for d in self.open_deals()
if period_filter is None or d.quarter == period_filter
)
return pipeline / quota if quota else 0.0
def scenario_summary(self, periods=None):
"""
Returns dict: period → {conservative, base, upside, open_pipeline}.
periods: list of month_keys to include; if None, all months.
"""
summaries = {}
all_months = sorted(set(d.month_key for d in self.open_deals()))
target_months = periods or all_months
for month in target_months:
deals_in_month = [d for d in self.open_deals() if d.month_key == month]
if not deals_in_month:
continue
summaries[month] = {
"deal_count": len(deals_in_month),
"open_pipeline": sum(d.arr_value for d in deals_in_month),
"conservative": sum(d.weighted_value(self.stage_probs, "conservative") for d in deals_in_month),
"base": sum(d.weighted_value(self.stage_probs, "base") for d in deals_in_month),
"upside": sum(d.weighted_value(self.stage_probs, "upside") for d in deals_in_month),
}
return summaries
def rep_performance(self):
"""Returns dict: rep → {pipeline, weighted_base, deal_count, avg_deal_size}."""
rep_data = defaultdict(lambda: {"pipeline": 0.0, "weighted_base": 0.0,
"deal_count": 0, "deals": []})
for deal in self.open_deals():
rep_data[deal.rep]["pipeline"] += deal.arr_value
rep_data[deal.rep]["weighted_base"] += deal.weighted_value(self.stage_probs, "base")
rep_data[deal.rep]["deal_count"] += 1
rep_data[deal.rep]["deals"].append(deal.arr_value)
result = {}
for rep, data in rep_data.items():
deals = data["deals"]
result[rep] = {
"pipeline": data["pipeline"],
"weighted_base": data["weighted_base"],
"deal_count": data["deal_count"],
"avg_deal_size": statistics.mean(deals) if deals else 0.0,
}
return result
def segment_breakdown(self, scenario="base"):
"""Returns dict: segment → weighted ARR."""
result = defaultdict(float)
for deal in self.open_deals():
result[deal.segment or "unspecified"] += deal.weighted_value(self.stage_probs, scenario)
return dict(result)
def stage_distribution(self):
"""Returns dict: stage → {count, total_arr, avg_arr}."""
result = defaultdict(lambda: {"count": 0, "total_arr": 0.0})
for deal in self.open_deals():
result[deal.stage]["count"] += 1
result[deal.stage]["total_arr"] += deal.arr_value
out = {}
for stage, data in result.items():
out[stage] = {
"count": data["count"],
"total_arr": data["total_arr"],
"avg_arr": data["total_arr"] / data["count"] if data["count"] else 0,
"probability": self.stage_probs.get(stage, 0.0),
}
return out
def confidence_interval(self, scenario="base", iterations=1000):
"""
Monte Carlo simulation to generate confidence interval around base forecast.
Each deal wins/loses based on its probability; runs iterations times.
Returns (p10, p50, p90) of total expected ARR.
"""
import random
random.seed(42)
totals = []
for _ in range(iterations):
total = 0.0
for deal in self.open_deals():
prob = min(1.0, self.stage_probs.get(deal.stage, 0.0) * SCENARIO_MULTIPLIERS[scenario])
if random.random() < prob:
total += deal.arr_value
totals.append(total)
totals.sort()
n = len(totals)
return (
totals[int(n * 0.10)], # P10 (conservative)
totals[int(n * 0.50)], # P50 (median)
totals[int(n * 0.90)], # P90 (upside)
)
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt_currency(value):
if value >= 1_000_000:
return f".2fM"
if value >= 1_000:
return f".1fK"
return f".0f"
def fmt_pct(value):
return f"{value * 100:.1f}%"
def print_header(title):
width = 70
print()
print("=" * width)
print(f" {title}")
print("=" * width)
def print_section(title):
print(f"\n--- {title} ---")
def print_report(engine, quota=None, current_quarter=None):
open_deals = engine.open_deals()
won_deals = engine.closed_won_deals()
print_header("REVENUE FORECAST MODEL")
print(f" Generated: {date.today().isoformat()}")
print(f" Open deals: {len(open_deals)}")
print(f" Closed Won (in dataset): {len(won_deals)}")
total_pipeline = sum(d.arr_value for d in open_deals)
total_won = sum(d.arr_value for d in won_deals)
print(f" Total open pipeline: {fmt_currency(total_pipeline)}")
print(f" Total closed won: {fmt_currency(total_won)}")
# ── Coverage ratio
if quota:
print_section("PIPELINE COVERAGE")
q = current_quarter or "this quarter"
ratio = engine.coverage_ratio(quota, period_filter=current_quarter)
status = "✅ Healthy" if ratio >= 3.0 else ("⚠️ Thin" if ratio >= 2.0 else "🔴 Critical")
print(f" Quota target: {fmt_currency(quota)}")
print(f" Coverage ratio: {ratio:.1f}x {status}")
print(f" (Minimum healthy = 3x; < 2x = pipeline emergency)")
# ── Stage distribution
print_section("STAGE DISTRIBUTION")
stage_dist = engine.stage_distribution()
col_w = [28, 8, 14, 12, 10]
header = f" {'Stage':<{col_w[0]}} {'Deals':>{col_w[1]}} {'Pipeline':>{col_w[2]}} {'Avg Size':>{col_w[3]}} {'Win Prob':>{col_w[4]}}"
print(header)
print(" " + "-" * (sum(col_w) + 4))
for stage, data in sorted(stage_dist.items(), key=lambda x: -x[1]["total_arr"]):
print(f" {stage:<{col_w[0]}} {data['count']:>{col_w[1]}} "
f"{fmt_currency(data['total_arr']):>{col_w[2]}} "
f"{fmt_currency(data['avg_arr']):>{col_w[3]}} "
f"{fmt_pct(data['probability']):>{col_w[4]}}")
# ── Scenario forecast by month
print_section("MONTHLY FORECAST — ALL SCENARIOS")
summaries = engine.scenario_summary()
col_w2 = [10, 8, 14, 14, 14, 14]
h2 = (f" {'Month':<{col_w2[0]}} {'Deals':>{col_w2[1]}} "
f"{'Pipeline':>{col_w2[2]}} {'Conservative':>{col_w2[3]}} "
f"{'Base':>{col_w2[4]}} {'Upside':>{col_w2[5]}}")
print(h2)
print(" " + "-" * (sum(col_w2) + 5))
for month, data in summaries.items():
print(f" {month:<{col_w2[0]}} {data['deal_count']:>{col_w2[1]}} "
f"{fmt_currency(data['open_pipeline']):>{col_w2[2]}} "
f"{fmt_currency(data['conservative']):>{col_w2[3]}} "
f"{fmt_currency(data['base']):>{col_w2[4]}} "
f"{fmt_currency(data['upside']):>{col_w2[5]}}")
# ── Quarterly rollup
print_section("QUARTERLY FORECAST ROLLUP")
q_conservative = defaultdict(float)
q_base = defaultdict(float)
q_upside = defaultdict(float)
q_pipeline = defaultdict(float)
q_count = defaultdict(int)
for deal in open_deals:
q_conservative[deal.quarter] += deal.weighted_value(engine.stage_probs, "conservative")
q_base[deal.quarter] += deal.weighted_value(engine.stage_probs, "base")
q_upside[deal.quarter] += deal.weighted_value(engine.stage_probs, "upside")
q_pipeline[deal.quarter] += deal.arr_value
q_count[deal.quarter] += 1
quarters = sorted(q_base.keys())
col_w3 = [10, 8, 14, 14, 14, 14]
h3 = (f" {'Quarter':<{col_w3[0]}} {'Deals':>{col_w3[1]}} "
f"{'Pipeline':>{col_w3[2]}} {'Conservative':>{col_w3[3]}} "
f"{'Base':>{col_w3[4]}} {'Upside':>{col_w3[5]}}")
print(h3)
print(" " + "-" * (sum(col_w3) + 5))
for q in quarters:
print(f" {q:<{col_w3[0]}} {q_count[q]:>{col_w3[1]}} "
f"{fmt_currency(q_pipeline[q]):>{col_w3[2]}} "
f"{fmt_currency(q_conservative[q]):>{col_w3[3]}} "
f"{fmt_currency(q_base[q]):>{col_w3[4]}} "
f"{fmt_currency(q_upside[q]):>{col_w3[5]}}")
# ── Monte Carlo confidence interval
print_section("CONFIDENCE INTERVAL (Monte Carlo, 1,000 simulations)")
p10, p50, p90 = engine.confidence_interval("base")
print(f" P10 (conservative floor): {fmt_currency(p10)}")
print(f" P50 (median expected): {fmt_currency(p50)}")
print(f" P90 (upside ceiling): {fmt_currency(p90)}")
print(f" Range spread: {fmt_currency(p90 - p10)}")
# ── Rep performance
print_section("REP PIPELINE PERFORMANCE")
rep_perf = engine.rep_performance()
if rep_perf:
col_w4 = [20, 8, 14, 14, 12]
h4 = (f" {'Rep':<{col_w4[0]}} {'Deals':>{col_w4[1]}} "
f"{'Pipeline':>{col_w4[2]}} {'Weighted':>{col_w4[3]}} {'Avg Size':>{col_w4[4]}}")
print(h4)
print(" " + "-" * (sum(col_w4) + 4))
for rep, data in sorted(rep_perf.items(), key=lambda x: -x[1]["pipeline"]):
print(f" {rep:<{col_w4[0]}} {data['deal_count']:>{col_w4[1]}} "
f"{fmt_currency(data['pipeline']):>{col_w4[2]}} "
f"{fmt_currency(data['weighted_base']):>{col_w4[3]}} "
f"{fmt_currency(data['avg_deal_size']):>{col_w4[4]}}")
# ── Segment breakdown
print_section("SEGMENT BREAKDOWN (Base Forecast)")
seg = engine.segment_breakdown("base")
for segment, value in sorted(seg.items(), key=lambda x: -x[1]):
bar_len = int((value / total_pipeline) * 30) if total_pipeline else 0
bar = "█" * bar_len
print(f" {segment:<20} {fmt_currency(value):>12} {bar}")
# ── Red flags
print_section("FORECAST HEALTH FLAGS")
flags = []
if total_pipeline > 0:
coverage = total_pipeline / quota if quota else None
if coverage and coverage < 2.0:
flags.append("🔴 Pipeline coverage below 2x — serious shortfall risk this quarter")
elif coverage and coverage < 3.0:
flags.append("⚠️ Pipeline coverage below 3x — limited buffer for slippage")
# Stage concentration risk
early_stage_pct = sum(
d.arr_value for d in open_deals
if engine.stage_probs.get(d.stage, 0) < 0.30
) / total_pipeline
if early_stage_pct > 0.60:
flags.append(f"⚠️ {fmt_pct(early_stage_pct)} of pipeline in early stages (< 30% probability)")
# Deal concentration
deal_values = sorted([d.arr_value for d in open_deals], reverse=True)
if deal_values and deal_values[0] / total_pipeline > 0.25:
flags.append(f"⚠️ Top deal is {fmt_pct(deal_values[0]/total_pipeline)} of pipeline — concentration risk")
# Spread between scenarios
total_conservative = sum(d.weighted_value(engine.stage_probs, "conservative") for d in open_deals)
total_upside = sum(d.weighted_value(engine.stage_probs, "upside") for d in open_deals)
spread = (total_upside - total_conservative) / total_conservative if total_conservative else 0
if spread > 0.40:
flags.append(f"⚠️ High scenario spread ({fmt_pct(spread)}) — forecast confidence is low")
if flags:
for f in flags:
print(f" {f}")
else:
print(" ✅ No critical flags detected")
print()
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
SAMPLE_CSV = """deal_id,name,stage,arr_value,close_date,rep,segment
D001,Acme Corp ERP Integration,negotiation,85000,2026-03-15,Sarah Chen,Enterprise
D002,TechStart PLG Expansion,proposal,28000,2026-03-28,Marcus Webb,Mid-Market
D003,Global Retail Co,verbal_commit,220000,2026-03-10,Sarah Chen,Enterprise
D004,BioLab Analytics,poc,62000,2026-04-05,Jamie Park,Mid-Market
D005,FinServ Holdings,demo,150000,2026-04-20,Sarah Chen,Enterprise
D006,MidWest Logistics,qualification,35000,2026-04-30,Marcus Webb,Mid-Market
D007,Edu Platform Inc,negotiation,42000,2026-03-25,Jamie Park,SMB
D008,Healthcare Connect,proposal,95000,2026-05-15,Sarah Chen,Enterprise
D009,Startup Hub Network,demo,18000,2026-04-10,Marcus Webb,SMB
D010,CloudOps Systems,poc,75000,2026-05-01,Jamie Park,Mid-Market
D011,National Bank Corp,verbal_commit,310000,2026-03-31,Sarah Chen,Enterprise
D012,RetailTech Co,qualification,22000,2026-05-20,Marcus Webb,SMB
D013,InsurTech Platform,negotiation,88000,2026-04-15,Jamie Park,Mid-Market
D014,GovTech Solutions,proposal,175000,2026-06-01,Sarah Chen,Enterprise
D015,AgriData Systems,demo,31000,2026-05-10,Marcus Webb,Mid-Market
D016,Legal AI Corp,poc,55000,2026-04-25,Jamie Park,Mid-Market
D017,Closed Won Deal,closed_won,120000,2026-02-15,Sarah Chen,Enterprise
D018,Lost Deal,closed_lost,45000,2026-02-20,Marcus Webb,Mid-Market
"""
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def load_deals_from_csv(csv_text):
reader = csv.DictReader(StringIO(csv_text))
deals = []
errors = []
for i, row in enumerate(reader, start=2):
try:
deal = Deal(
deal_id=row.get("deal_id", f"row_{i}"),
name=row.get("name", ""),
stage=row.get("stage", ""),
arr_value=row.get("arr_value", 0),
close_date=row.get("close_date", ""),
rep=row.get("rep", ""),
segment=row.get("segment", ""),
)
deals.append(deal)
except (ValueError, KeyError) as e:
errors.append(f" Row {i}: {e}")
if errors:
print("⚠️ Skipped rows with errors:")
for err in errors:
print(err)
return deals
def main():
parser = argparse.ArgumentParser(
description="Revenue Forecast Model — pipeline-based ARR forecasting"
)
parser.add_argument(
"--csv", metavar="FILE",
help="CSV file with pipeline data (uses sample data if not provided)"
)
parser.add_argument(
"--quota", type=float, default=1_000_000,
help="Quarterly quota target in ARR (default: $1,000,000)"
)
parser.add_argument(
"--quarter", metavar="QUARTER",
help='Current quarter filter e.g. "Q2 2026" (optional)'
)
parser.add_argument(
"--scenario", choices=["conservative", "base", "upside"],
default="base",
help="Primary scenario to report (default: base)"
)
parser.add_argument(
"--json", action="store_true",
help="Output forecast as JSON instead of formatted report"
)
args = parser.parse_args()
# Load data
if args.csv:
try:
with open(args.csv, "r", encoding="utf-8") as f:
csv_text = f.read()
except FileNotFoundError:
print(f"Error: File not found: {args.csv}", file=sys.stderr)
sys.exit(1)
else:
print("No --csv provided. Using sample pipeline data.\n")
csv_text = SAMPLE_CSV
deals = load_deals_from_csv(csv_text)
if not deals:
print("No deals loaded. Exiting.", file=sys.stderr)
sys.exit(1)
# Calibrate win rates from closed deals
historical_probs = calculate_historical_win_rates(deals)
stage_probs = historical_probs if historical_probs else DEFAULT_STAGE_PROBABILITIES
engine = ForecastEngine(deals, stage_probs=stage_probs)
if args.json:
output = {
"generated": date.today().isoformat(),
"quota": args.quota,
"open_pipeline": sum(d.arr_value for d in engine.open_deals()),
"coverage_ratio": engine.coverage_ratio(args.quota, args.quarter),
"monthly_forecast": engine.scenario_summary(),
"quarterly_base": engine.pipeline_by_quarter("base"),
"confidence_interval": dict(zip(
["p10", "p50", "p90"],
engine.confidence_interval("base")
)),
"rep_performance": engine.rep_performance(),
"segment_breakdown": engine.segment_breakdown("base"),
}
print(json.dumps(output, indent=2))
else:
print_report(engine, quota=args.quota, current_quarter=args.quarter)
if __name__ == "__main__":
main()
Financial leadership for startups and scaling companies. Financial modeling, unit economics, fundraising strategy, cash management, and board financial packa...
---
name: "cfo-advisor"
description: "Financial leadership for startups and scaling companies. Financial modeling, unit economics, fundraising strategy, cash management, and board financial packages. Use when building financial models, analyzing unit economics, planning fundraising, managing cash runway, preparing board materials, or when user mentions CFO, burn rate, runway, fundraising, unit economics, LTV, CAC, term sheets, or financial strategy."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: cfo-leadership
updated: 2026-03-05
python-tools: burn_rate_calculator.py, unit_economics_analyzer.py, fundraising_model.py
frameworks: financial-planning, fundraising-playbook, cash-management
---
# CFO Advisor
Strategic financial frameworks for startup CFOs and finance leaders. Numbers-driven, decisions-focused.
This is **not** a financial analyst skill. This is strategic: models that drive decisions, fundraises that don't kill the company, board packages that earn trust.
## Keywords
CFO, chief financial officer, burn rate, runway, unit economics, LTV, CAC, fundraising, Series A, Series B, term sheet, cap table, dilution, financial model, cash flow, board financials, FP&A, SaaS metrics, ARR, MRR, net dollar retention, gross margin, scenario planning, cash management, treasury, working capital, burn multiple, rule of 40
## Quick Start
```bash
# Burn rate & runway scenarios (base/bull/bear)
python scripts/burn_rate_calculator.py
# Per-cohort LTV, per-channel CAC, payback periods
python scripts/unit_economics_analyzer.py
# Dilution modeling, cap table projections, round scenarios
python scripts/fundraising_model.py
```
## Key Questions (ask these first)
- **What's your burn multiple?** (Net burn ÷ Net new ARR. > 2x is a problem.)
- **If fundraising takes 6 months instead of 3, do you survive?** (If not, you're already behind.)
- **Show me unit economics per cohort, not blended.** (Blended hides deterioration.)
- **What's your NDR?** (> 100% means you grow without signing a single new customer.)
- **What are your decision triggers?** (At what runway do you start cutting? Define now, not in a crisis.)
## Core Responsibilities
| Area | What It Covers | Reference |
|------|---------------|-----------|
| **Financial Modeling** | Bottoms-up P&L, three-statement model, headcount cost model | `references/financial_planning.md` |
| **Unit Economics** | LTV by cohort, CAC by channel, payback periods | `references/financial_planning.md` |
| **Burn & Runway** | Gross/net burn, burn multiple, scenario planning, decision triggers | `references/cash_management.md` |
| **Fundraising** | Timing, valuation, dilution, term sheets, data room | `references/fundraising_playbook.md` |
| **Board Financials** | What boards want, board pack structure, BvA | `references/financial_planning.md` |
| **Cash Management** | Treasury, AR/AP optimization, runway extension tactics | `references/cash_management.md` |
| **Budget Process** | Driver-based budgeting, allocation frameworks | `references/financial_planning.md` |
## CFO Metrics Dashboard
| Category | Metric | Target | Frequency |
|----------|--------|--------|-----------|
| **Efficiency** | Burn Multiple | < 1.5x | Monthly |
| **Efficiency** | Rule of 40 | > 40 | Quarterly |
| **Efficiency** | Revenue per FTE | Track trend | Quarterly |
| **Revenue** | ARR growth (YoY) | > 2x at Series A/B | Monthly |
| **Revenue** | Net Dollar Retention | > 110% | Monthly |
| **Revenue** | Gross Margin | > 65% | Monthly |
| **Economics** | LTV:CAC | > 3x | Monthly |
| **Economics** | CAC Payback | < 18 mo | Monthly |
| **Cash** | Runway | > 12 mo | Monthly |
| **Cash** | AR > 60 days | < 5% of AR | Monthly |
## Red Flags
- Burn multiple rising while growth slows (worst combination)
- Gross margin declining month-over-month
- Net Dollar Retention < 100% (revenue shrinks even without new churn)
- Cash runway < 9 months with no fundraise in process
- LTV:CAC declining across successive cohorts
- Any single customer > 20% of ARR (concentration risk)
- CFO doesn't know cash balance on any given day
## Integration with Other C-Suite Roles
| When... | CFO works with... | To... |
|---------|-------------------|-------|
| Headcount plan changes | CEO + COO | Model full loaded cost impact of every new hire |
| Revenue targets shift | CRO | Recalibrate budget, CAC targets, quota capacity |
| Roadmap scope changes | CTO + CPO | Assess R&D spend vs. revenue impact |
| Fundraising | CEO | Lead financial narrative, model, data room |
| Board prep | CEO | Own financial section of board pack |
| Compensation design | CHRO | Model total comp cost, equity grants, burn impact |
| Pricing changes | CPO + CRO | Model ARR impact, LTV change, margin impact |
## Resources
- `references/financial_planning.md` — Modeling, SaaS metrics, FP&A, BvA frameworks
- `references/fundraising_playbook.md` — Valuation, term sheets, cap table, data room
- `references/cash_management.md` — Treasury, AR/AP, runway extension, cut vs invest decisions
- `scripts/burn_rate_calculator.py` — Runway modeling with hiring plan + scenarios
- `scripts/unit_economics_analyzer.py` — Per-cohort LTV, per-channel CAC
- `scripts/fundraising_model.py` — Dilution, cap table, multi-round projections
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- Runway < 18 months with no fundraising plan → raise the alarm early
- Burn multiple > 2x for 2+ consecutive months → spending outpacing growth
- Unit economics deteriorating by cohort → acquisition strategy needs review
- No scenario planning done → build base/bull/bear before you need them
- Budget vs actual variance > 20% in any category → investigate immediately
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "How much runway do we have?" | Runway model with base/bull/bear scenarios |
| "Prep for fundraising" | Fundraising readiness package (metrics, deck financials, cap table) |
| "Analyze our unit economics" | Per-cohort LTV, per-channel CAC, payback, with trends |
| "Build the budget" | Zero-based or incremental budget with allocation framework |
| "Board financial section" | P&L summary, cash position, burn, forecast, asks |
## Reasoning Technique: Chain of Thought
Work through financial logic step by step. Show all math. Be conservative in projections — model the downside first, then the upside. Never round in your favor.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:references/cash_management.md
# Cash Management Reference
Cash is the oxygen of a startup. You can be unprofitable for years. You cannot be out of cash for a day.
---
## 1. Cash Flow Management
### The Cash Equation
```
Ending Cash = Beginning Cash
+ Cash collected from customers
- Cash paid to employees
- Cash paid to vendors
- Cash paid for infrastructure
- Debt service
+/- Financing activities
Note: This is NOT the P&L. Revenue recognition ≠ cash collected.
```
### Where Cash Hides (and Leaks)
**Cash sources you might be under-using:**
- Deferred revenue (annual billing locks in cash 12 months early)
- Customer deposits on enterprise contracts
- Vendor payment terms (Net 60 instead of Net 30 = free float)
- AWS/GCP startup credits (often $25K–$100K available, widely unused)
- Revenue-based financing on predictable MRR
- Venture debt (non-dilutive, available post-Series A)
**Cash drains that sneak up on you:**
- Annual software licenses paid in Q1 (budget for the lump sum)
- Event sponsorships (often 6-12 months in advance)
- Recruiting fees (15-25% of first-year salary, due on hire)
- Legal fees (data room prep, fundraise close = $50K–$200K surprise)
- Late-paying enterprise customers (Net 60 in contract, pays Net 90 in practice)
### Cash Flow vs P&L: The Gap
**Scenario: $1M enterprise deal signed December 31**
```
P&L impact (accrual):
December revenue: $83K (1/12 of annual)
Cash impact:
If billed annually upfront: +$1,000K in December (GREAT)
If billed quarterly: +$250K in December (good)
If billed monthly: +$83K in December (fine)
If Net 60 terms: +$0 in December, +$83K in February (cash drag)
```
**The CFO's job:** Maximize the timing difference between cash in and cash out.
- Collect from customers as early as possible (annual upfront, early payment discounts)
- Pay vendors as late as possible (maximize payment terms)
- Never confuse deferred revenue (a liability) with actual cash (it is cash — just count it right)
---
## 2. Treasury and Banking Strategy
### Account Structure
```
Operating Account (primary bank):
Balance: 3-6 months of operating expenses
Purpose: Payroll, vendor payments, day-to-day ops
Product: Business checking or high-yield business savings
Bank: Chase, SVB successor (First Citizens), Mercury, Brex
Reserve Account (secondary or same bank):
Balance: Everything above operating float
Purpose: Reserve; move to operating as needed
Product: Money market fund or T-Bill ladder
Target yield (2024-2025): 4.5%–5.2%
Products: Vanguard VMFXX, Fidelity SPAXX, or direct T-Bills via TreasuryDirect
Emergency Account (separate bank):
Balance: 1-2 months expenses
Purpose: If primary bank has issues (SVB taught this lesson)
Product: Business savings
```
**FDIC coverage:** $250K per depositor per institution. For balances above $250K at a single bank, either:
- Use CDARS/ICS (bank sweeps into multiple FDIC-insured accounts automatically)
- Spread across multiple banks
- Move excess to T-Bills (backed by US government, not FDIC, but safer)
**After SVB (March 2023):** Every CFO should have at least 2 banking relationships. If one bank fails or freezes, you can make payroll.
### Yield on Cash
At $3M cash, the difference between 0% (checking) and 5% (T-Bills) is $150K/year.
That's a month of runway for a $150K/month burn company. **Get yield on reserves.**
```
Monthly yield on $3M at 5%: ~$12,500
Annual: ~$150,000
This is not optional. Set it up once and automate.
```
---
## 3. AR/AP Optimization
### Accounts Receivable: Get Paid Faster
**Billing model impact on cash:**
```
Annual Upfront Quarterly Monthly Net 30 Monthly
Cash Day 1: 100% of ACV 25% of ACV 8.3% 0%
Cash Month 2: 0% (done) 0% 8.3% 8.3%
12-month total: 100% 100% 100% 100%
For $100K ACV customer, Year 1 cash:
Annual upfront: $100K immediately
Monthly Net 30: $8.3K × 11 months = $91.7K (1 month lag)
Cash benefit: $100K vs $91.7K = $8.3K benefit + no collection risk
```
**Push for annual billing. Make it easy with a discount:**
```
"Pay annually and get 2 months free (16% discount)"
Most SMB customers will take this.
Enterprise: use MSA structure with annual invoicing, not month-to-month.
```
**AR Aging Policy:**
```
> 0-30 days: Current. No action.
> 30-60 days: Friendly reminder from AR team.
> 60-90 days: Escalate to Customer Success.
> 90 days: CFO or CEO-level outreach. Consider collections.
> 120 days: Reserve for bad debt. Legal/collections.
Reserve policy: 50% of 90-120 day AR, 100% of > 120 days
```
**What slows down collections:**
- Wrong contact (billing contact vs. user) — get finance contact during onboarding
- Enterprise PO required — know this upfront, not when invoice is due
- Credit holds or budget freeze — your CSM should surface these early
- Invoice errors — every wrong invoice extends payment by 30-60 days
### Accounts Payable: Pay Slower
**Standard terms by vendor type:**
```
SaaS tools: Net 30 default. Push for Net 45 or Net 60 at scale.
Cloud providers: Pay as you go. Apply for credits first.
Professional services (agencies, lawyers): Net 30 minimum. Get Net 45 where possible.
Rent/office: Whatever the lease says. Negotiate quarterly payments if you can.
Payroll: Pay on time. Never delay payroll. Ever.
```
**Early payment discount trap:**
```
"2/10 Net 30" means: 2% discount if you pay in 10 days, else pay in 30.
Annual cost of NOT taking this: 2% × (365/(30-10)) = ~36% APY
ALWAYS take early payment discounts > 2%.
Never take discounts < 1%.
```
**AP workflow:**
1. All invoices → finance inbox (not individual employees)
2. Approval required above threshold ($500 for startups)
3. Pay at end of terms, not when invoice arrives
4. Batch payments weekly (not daily) to reduce processing overhead
---
## 4. Runway Extension Tactics
Use these when you need to extend runway without raising. Ranked by speed and impact.
### Tier 1: Fast Cash (Days)
**Annual billing campaign:**
```
Target: Existing monthly customers
Offer: 2 months free (16% discount) or 1 month free (8% discount) for annual upfront
Process: CSM-led email campaign to all monthly customers
Impact: $X MRR × 12 × conversion rate = immediate cash injection
Timeline: 2-4 weeks
No dilution. No debt. High impact.
```
**Prepayment incentive for pipeline:**
```
For deals in late stage, offer annual upfront pricing with 10-15% discount.
Close rate may increase. Cash timing dramatically improves.
```
### Tier 2: Cost Control (2-4 Weeks)
**Hiring freeze:**
```
Every unfilled role = salary × 1.25 per month.
For a 30-person company, 3 open roles at $150K average:
Monthly savings: 3 × $150K × 1.25 / 12 = $47K/month
Over 6 months: $280K
Impact: Immediate. No blood.
```
**Software audit:**
```
Pull all credit card charges and ACH debits.
Cancel any subscription not used in 30 days.
Typical savings: $3K-$15K/month at Series A stage.
Tools: Vendr, Spendesk, or just a spreadsheet of recurring charges.
```
**Cloud cost optimization:**
```
Right-size instances (dev/staging don't need prod-scale)
Reserve instances (1-year reserved = 30-40% savings vs on-demand)
Delete unused resources (load balancers, IPs, old snapshots)
Typical savings: 20-35% of current cloud bill
```
### Tier 3: Vendor Renegotiation (2-6 Weeks)
**Payment term extension:**
```
Ask key vendors for Net 60 instead of Net 30.
$500K in AP × 30 days = $500K × (30/365) = ~$41K cash float improvement
Won't always work, but vendors often say yes to good customers.
```
**Renewal timing:**
```
Push annual renewals to later in the year.
Preserve cash for Q1 (typically heaviest sales hiring quarter).
```
**Vendor credits:**
```
AWS: AWS Activate (up to $100K for qualified startups)
GCP: Google for Startups (up to $200K)
Azure: Microsoft for Startups (up to $150K)
Stripe: Revenue share programs
Hubspot: Startup pricing (90% off)
```
### Tier 4: Financing (Weeks to Months)
**Revenue-based financing:**
```
Providers: Clearco, Capchase, Pipe, Arc
Structure: Advance 3-6 months of MRR. Repay with % of monthly revenue.
Cost: Typically 6-12% annualized.
Speed: 1-2 weeks to close.
When to use: Bridge to next ARR milestone before raising equity.
When NOT to use: When burn rate is structural (will consume the advance fast).
```
**Venture debt:**
```
Providers: SVB (now First Citizens), Western Technology Investment, Hercules, TriplePoint
Structure: Term loan, typically 3-6x monthly gross burn
Interest: Prime + 2-4% + warrants
When available: Post-Series A, when revenue is predictable
Typical timing: Add alongside an equity round (don't raise debt when you need equity)
Impact: Extends runway 3-6 months without dilution
When NOT to use: If you might trip financial covenants (minimum cash, revenue)
```
**Convertible bridge:**
```
Existing investors write bridge note: $500K-$2M at favorable terms.
Structure: Converts at discount (10-20%) or cap into next equity round.
When to use: You're 60-90 days from closing an equity round and need cash to get there.
When NOT to use: As a long-term strategy. Bridge-to-bridge is a death spiral.
```
### Tier 5: Structural Cost Reduction (Weeks + Impact on Morale)
**Salary deferrals (founders first):**
```
Founders take 20-30% salary reduction, accrued for future repayment.
Signals commitment to team and investors.
Only ask employees to follow if founders go first.
Always pay market rate to key non-founder employees — you can't afford to lose them.
```
**Reduction in force (RIF):**
```
Threshold: If burn multiple > 3x and growth < 20% YoY, a RIF is likely necessary.
Sizing: Model to achieve at least 12 months runway without fundraising.
Rule: Don't do a RIF twice. Size it right the first time.
Two small RIFs destroy morale worse than one decisive one.
Process: Legal counsel required. WARN Act (60-day notice) if > 100 employees.
Focus cuts: G&A and underperforming sales roles first. Protect engineering and key revenue.
```
---
## 5. When to Cut vs When to Invest
### The Framework
**Cut when:**
- Burn multiple > 2x and growth is decelerating
- Runway < 9 months with no fundraise imminent
- LTV:CAC declining for 3+ consecutive months
- Any spend category with no measurable return in 90 days
- Headcount in functions not directly tied to near-term revenue or product-market fit
**Invest when:**
- Magic number > 1 (every dollar in S&M returns > $1 in gross profit)
- LTV:CAC > 3x in a specific channel (pour money in)
- Gross margin > 70% (unit economics are healthy; growth is the constraint)
- Cohort data improving (retention getting better → LTV going up → invest in growth)
- CAC payback < 12 months (you get your money back fast enough to keep reinvesting)
### The False Economy Trap
**Don't cut:**
- Top-of-funnel demand gen that generates qualified pipeline (if CAC payback is < 12 months, this is your best investment)
- Engineering capacity on core product (technical debt compounds and slows you down permanently)
- Key account managers on your largest customers (churn from top customers is catastrophic)
**Cut these first:**
- Conference sponsorships with no measurable pipeline
- Tools and subscriptions with < 5 users or < 30% utilization
- Agency spend that could be done in-house
- Roadmap items that aren't tied to retention or expansion revenue
- Any G&A spend that isn't legally required
### Decision Triggers (Pre-Define These)
Don't make these decisions in a crisis. Define the triggers now:
```
At 12 months runway: Review all discretionary spend. Start fundraise process.
At 9 months runway: Implement hiring freeze. Fundraise is mandatory.
At 6 months runway: Cut non-essential spend 20%. If no fundraise term sheet, run RIF model.
At 4 months runway: Execute RIF. Explore all financing options. Notify board.
At 3 months runway: Emergency plan only. All options on table (bridge, strategic, wind down).
```
---
## Key Formulas
```python
# Net burn
net_burn = gross_burn - revenue_collected
# Runway (months)
runway_months = cash_balance / net_burn
# Cash conversion cycle
ccc = days_sales_outstanding + days_inventory_held - days_payable_outstanding
# Lower CCC = better cash efficiency
# Days Sales Outstanding (DSO)
dso = (accounts_receivable / revenue) * 30 # monthly revenue
# Days Payable Outstanding (DPO)
dpo = (accounts_payable / cogs) * 30 # target: maximize this
# Working capital
working_capital = current_assets - current_liabilities
# Quick ratio (liquidity)
quick_ratio_liquidity = (cash + ar) / current_liabilities
# Target: > 1.5 (you can pay short-term obligations without selling assets)
# Free cash flow
fcf = operating_cash_flow - capex
```
FILE:references/financial_planning.md
# Financial Planning Reference
Startup financial modeling frameworks. Build models that drive decisions, not models that impress investors.
---
## 1. Startup Financial Modeling
### Bottoms-Up vs Top-Down
**Top-down model (don't use for operating):**
```
TAM = $10B
SOM = 1% = $100M
Revenue = $100M in year 5
```
This is marketing. You cannot manage a company against these numbers.
**Bottoms-up model (use this):**
```
Year 1 Revenue Build:
Sales headcount: 3 AEs by Q1, +2 in Q2, +3 in Q4
Ramp curve: Month 1-3 = 25%, Month 4-6 = 75%, Month 7+ = 100%
Quota per ramped AE: $600K ARR
Effective quota (weighted for ramp): $1.2M ARR in Year 1
Win rate: 25%
Average deal: $48K ACV
Pipeline needed: $1.2M / 25% = $4.8M ARR pipeline
Required meetings to create that pipeline: $4.8M / (conversion 20%) / ($48K ACV × 0.5 to meeting) = ~200 meetings
```
Now you have something actionable. You know how many SDR calls, how many marketing leads, what conversion rate you need to hold. Every assumption is visible and challengeable.
### Building the Operating Model
#### Revenue Engine
**New ARR Model (SaaS):**
```
Month N New ARR:
= Quota-carrying reps (fully ramped equivalent)
× Attainment rate (typically 70-80% of quota)
× Average deal size
+ PLG / self-serve (if applicable)
Quota-carrying reps (ramped equivalent):
= Sum(each rep × their ramp factor)
Ramp schedule:
Month 1-2: 0% (onboarding)
Month 3: 25%
Month 4-6: 50%
Month 7-9: 75%
Month 10+: 100%
```
**ARR Bridge (most important recurring visual):**
```
Beginning ARR
+ New ARR (new logos)
+ Expansion ARR (upsells, seat growth)
- Churned ARR (cancellations)
- Contraction ARR (downgrades)
= Ending ARR
Net ARR Added = New + Expansion - Churn - Contraction
Net Dollar Retention (NDR):
= (Beginning ARR + Expansion - Churn - Contraction) / Beginning ARR × 100
Target: > 110% for growth-stage SaaS
World-class: > 130% (Snowflake, Twilio-tier)
```
**MRR and ARR Relationship:**
```
ARR = MRR × 12 (simple, always use this)
Never mix monthly and annual contracts in MRR without normalization.
Annual contract booked = ACV / 12 = monthly contribution to ARR
Multi-year contracts: book each year at annual value (not multi-year total)
```
#### Headcount Model
Headcount is usually 60-80% of total costs. Model it carefully.
```
For each role:
- Start date
- Department
- Annual salary (from salary bands)
- Loaded cost (salary × 1.25-1.45 depending on benefits + recruiting method)
- Productive from (ramp period)
- Impact on revenue (for revenue-generating roles)
Total headcount cost = Σ (each FTE × loaded cost × months active / 12)
```
**Department headcount ratios (Series A benchmarks):**
```
Sales (S&M): 20-30% of headcount
Engineering/Product (R&D): 40-50% of headcount
Customer Success: 15-20% of headcount
G&A: 10-15% of headcount
```
#### COGS Model
Gross margin is the most important long-term indicator of business quality.
**COGS for SaaS:**
```
1. Hosting / Infrastructure (AWS, GCP, Azure)
- Scale with customer count or usage
- Should be 5-15% of ARR for mature SaaS
- If > 20%: infrastructure optimization needed
2. Customer Success headcount
- Ratio: 1 CSM per $1M-$3M ARR (varies by segment)
- SMB: 1 CSM per $500K ARR (high-touch required)
- Enterprise: 1 CSM per $2-5M ARR (strategic accounts)
3. Third-party licensing / APIs
- Per-customer or usage-based pass-through costs
- Critical to model at scale (margin killer if not tracked)
4. Payment processing
- 2.2-2.9% of revenue for Stripe/Braintree
- Can negotiate to 1.8-2.2% at scale (> $5M ARR)
```
**Gross Margin targets:**
```
SaaS: > 65% acceptable, > 75% good, > 80% exceptional
Marketplace: 50-70%
Hardware + software: 40-60%
Services + software: 30-50%
```
**If gross margin < 65%:**
- Infrastructure cost optimization (rightsizing, reserved instances)
- CS headcount review (automation, pooled CSMs)
- Pricing model review (usage-based pricing if cost is usage-driven)
- Third-party cost renegotiation
#### Opex Model
```
Sales & Marketing:
- AE/SDR/SE salaries + OTE (on-target earnings)
- Marketing programs (demand gen budget)
- Tools and technology (CRM, SEO, ads platforms)
- Events and travel
- Benchmark: 40-60% of revenue at growth stage, targeting < 30% at scale
Research & Development:
- Engineering salaries
- Product management
- Design
- Technical infrastructure for development
- Benchmark: 20-35% of revenue
General & Administrative:
- Finance, legal, HR, admin
- Office costs
- SaaS tools / software licenses
- D&O insurance
- Benchmark: 8-15% (target < 10% at scale)
```
### Financial Model Do's and Don'ts
| Do | Don't |
|----|-------|
| Build assumptions tab with all inputs | Hardcode numbers in formulas |
| Model monthly (not quarterly) at early stage | Use annual model for first 3 years |
| Start with headcount plan, build costs from it | Guess at expense line items |
| Show model to actual customers or users | Show model to investors before internal stress-test |
| Version your model | Overwrite old versions |
| Reconcile cash flow to P&L monthly | Trust P&L without cash flow model |
| Include a sensitivity table | Present single-scenario forecast |
---
## 2. Three-Statement Model for Startups
### Why All Three Matter
The P&L tells you if you're profitable. The cash flow statement tells you if you're alive. The balance sheet tells you if you're solvent.
Startups that only track P&L miss the gap between revenue recognition and cash collection.
### P&L Structure
```
Q1 Q2 Q3 Q4 FY
Revenue
Subscription ARR $400K $520K $680K $840K $2,440K
Professional Svcs $40K $50K $60K $65K $215K
Total Revenue $440K $570K $740K $905K $2,655K
COGS
Infrastructure $35K $42K $52K $62K $191K
CS Headcount $75K $75K $100K $100K $350K
3rd Party Licensing $15K $18K $22K $28K $83K
Total COGS $125K $135K $174K $190K $624K
Gross Profit $315K $435K $566K $715K $2,031K
Gross Margin 71.6% 76.3% 76.5% 79.0% 76.5%
Operating Expenses
Sales & Marketing $380K $420K $480K $520K $1,800K
Research & Dev $320K $340K $380K $400K $1,440K
General & Admin $120K $130K $140K $150K $540K
Total Opex $820K $890K $1000K $1070K $3,780K
EBITDA ($505K) ($455K) ($434K) ($355K) ($1,749K)
EBITDA Margin (114.8%)(79.8%) (58.6%) (39.2%) (65.9%)
```
### Cash Flow Statement
```
Q1 Q2 Q3 Q4
Operating Activities
Net Income ($510K) ($460K) ($440K) ($360K)
Add: D&A $8K $8K $8K $10K
Working Capital Changes:
AR increase ($45K) ($50K) ($60K) ($55K)
AP increase $20K $15K $20K $15K
Deferred Rev change $80K $60K $80K $90K
Operating Cash Flow ($447K) ($427K) ($392K) ($300K)
Investing Activities
Capex ($15K) ($8K) ($10K) ($12K)
Free Cash Flow ($462K) ($435K) ($402K) ($312K)
Financing Activities
None $0 $0 $0 $0
Net Change in Cash ($462K) ($435K) ($402K) ($312K)
Beginning Cash $3,500K $3,038K $2,603K $2,201K
Ending Cash $3,038K $2,603K $2,201K $1,889K
Runway (months) 13.1 12.1 10.9 10.1
```
**Key insight from this model:**
The deferred revenue offset (customers paying annually upfront) is reducing cash burn by ~$80-90K/quarter versus a pure monthly billing model. This is the CFO's lever — push for annual billing.
### Balance Sheet: The Startup Version
At early stage, track these specifically:
```
Assets:
Cash: Your lifeline. Monitor daily.
Accounts Receivable: What customers owe you. Age it monthly.
Prepaid Expenses: Software licenses, insurance paid upfront.
Liabilities:
Accounts Payable: What you owe vendors. Maximize terms.
Accrued Liabilities: Salaries owed, commissions earned but not paid.
Deferred Revenue: Customer prepayments. Liability until service delivered, but cash is yours.
Debt/Convertible Notes: Face value + interest accrual.
Equity:
Common Stock: Founder shares
Preferred Stock: Investor shares
APIC: Additional paid-in capital
Accumulated Deficit: Your running losses (expected for startups)
```
---
## 3. SaaS Metrics That Matter
### The Hierarchy of SaaS Metrics
```
Tier 1 (existential): ARR, Runway, Net Dollar Retention
Tier 2 (strategic): Gross Margin, Burn Multiple, LTV:CAC
Tier 3 (operational): CAC Payback, Churn Rate, ACV
Tier 4 (diagnostic): Logo Churn vs Revenue Churn, Expansion Rate, NPS
```
Never report Tier 4 metrics to your board if Tier 1 metrics are off-track.
### Core Metric Definitions
**ARR (Annual Recurring Revenue):**
```
ARR = Sum of all active annual contract values (normalized to annual)
What it is NOT: bookings, billings, or TCV
When to use MRR: Companies with mostly monthly contracts
When to use ARR: Companies with majority annual contracts
```
**Net Dollar Retention (NDR / NRR):**
```
NDR = (Beginning MRR + Expansion MRR - Churned MRR - Contraction MRR)
/ Beginning MRR × 100
The benchmark everyone quotes: 100% means existing customers are flat.
> 100% means existing customers grow revenue on their own.
World-class (Snowflake, Datadog): 130%+
Why it matters: NDR > 100% means revenue growth even if you sign zero new customers.
At NDR = 120% and $5M ARR: you will reach $7M ARR in 24 months without a single new sale.
```
**Gross Revenue Retention (GRR):**
```
GRR = (Beginning MRR - Churned MRR - Contraction MRR) / Beginning MRR × 100
GRR measures the floor of your retention (ignoring expansion).
GRR is always ≤ NDR.
Target: > 85% for SMB SaaS, > 90% for mid-market, > 95% for enterprise.
```
**Logo Churn vs Revenue Churn:**
```
Logo churn: % of customers who cancel (ignores size)
Revenue churn: % of ARR that cancels (accounts for size)
Why the distinction matters:
You could have 10% logo churn but 3% revenue churn (churning small customers)
Or 5% logo churn but 12% revenue churn (churning large customers) — much worse
Report both. If they diverge significantly, investigate immediately.
```
**ACV (Annual Contract Value):**
```
ACV = Total contract value / contract term in years
Not to be confused with ARR (which only counts recurring, not one-time fees)
Rising ACV: You're moving upmarket (good for efficiency, check if ICP is changing)
Falling ACV: You're moving downmarket (check burn multiple — may not be economic)
```
**Rule of 40:**
```
Rule of 40 = Revenue Growth Rate % + EBITDA Margin %
Target: > 40%
Example: 60% growth + (-15%) EBITDA margin = 45. Passing.
Example: 20% growth + 5% EBITDA margin = 25. Failing at growth stage.
At early stage (< $5M ARR): Rule of 40 doesn't apply. Growth is the only metric.
At growth stage ($5-20M ARR): Starting to matter.
At scale ($20M+ ARR): Board and investors will hold you to this.
```
---
## 4. FP&A for Startups: What to Measure When
### Metrics by Stage
**Pre-seed / Seed (< $1M ARR):**
```
Focus on: Cash, pipeline, customer conversations
Measure: Monthly cash burn, weeks of runway, NPS / customer satisfaction
Don't obsess over: EBITDA margin, gross margin (too early)
Frequency: Weekly cash check, monthly everything else
```
**Series A ($1-5M ARR):**
```
Focus on: Repeatable sales, unit economics
Measure: MRR growth, LTV:CAC, CAC payback by channel, gross margin
Don't obsess over: Profitability, G&A efficiency
Build now: Monthly financial close (< 5 business days), basic FP&A model
Frequency: Monthly board pack, weekly leadership metrics
```
**Series B ($5-20M ARR):**
```
Focus on: Scalable go-to-market, operational efficiency
Measure: NDR, burn multiple, revenue per FTE, OKR attainment
Start building: Budget vs actuals, department-level P&L
Build now: Finance team (first financial controller), ERP or NetSuite
Frequency: Monthly board pack + quarterly deep dive
```
**Series C+ ($20M+ ARR):**
```
Focus on: Path to profitability, market leadership
Measure: Rule of 40, free cash flow, CAC efficiency by segment
Must have: FP&A team, full three-statement model, 5-year plan
Frequency: Monthly financial close (< 3 business days), quarterly earnings prep
```
### Reporting Cadence
**Weekly (CFO + leadership):**
- Cash balance (CFO checks daily, reports weekly)
- Pipeline / sales metrics (if in a sales-led motion)
- Any metric that changed dramatically vs. prior week
**Monthly (board + leadership):**
- Full financial dashboard (ARR, gross margin, burn, runway)
- Budget vs actual with explanations for > 10% variances
- Unit economics update
- Headcount change summary
**Quarterly (board + investors):**
- Full three-statement model vs budget
- Cohort analysis update
- Scenario planning review and trigger assessment
- Next quarter outlook
---
## 5. Budget vs Actual Analysis Framework
### The Purpose of BvA
Budget vs actual is not about being right. It's about understanding *why* you were wrong, so you can make better decisions.
The CFO who reports "we missed budget by 15%" without explanation is failing. The CFO who says "we missed budget by 15% because enterprise deals took 30 more days to close than modeled — here's what we're doing about it" is doing their job.
### BvA Template
```
Category Budget Actual $ Var % Var Explanation
-------------------------------------------------------------------
ARR $2,400K $2,280K ($120K) (5%) 2 enterprise deals slipped to Q1
New ARR $400K $350K ($50K) (13%) Above
Expansion ARR $120K $140K $20K 17% PLG motion outperforming
Churn ($60K) ($80K) ($20K) (33%) 2 unexpected SMB churns (now fixed)
Gross Margin 75.0% 73.2% -1.8% n/a Infrastructure over-provisioned
S&M Spend $820K $840K ($20K) (2%) Within tolerance
R&D Spend $680K $710K ($30K) (4%) Backfill hire started month early
G&A Spend $140K $148K ($8K) (6%) Legal fees for new customer contract
Cash Burn (net) $580K $648K ($68K) (12%) Driven by ARR shortfall + costs
Runway (mo) 14.5 13.0 (1.5) n/a Tracking; fundraise target unchanged
```
### Variance Thresholds
```
< ±5%: Note in appendix, no explanation needed in main pack
5-10%: One-line explanation required
> 10%: Full paragraph: what happened, why, what changes
> 20%: Board conversation required (model assumption was wrong, or unexpected event)
```
### Forecasting vs Budgeting
**Budget:** Set at start of year. Fixed expectation. Updated quarterly.
**Forecast:** Rolling 3-month outlook. Updated monthly. Should converge with budget over time.
```
Common mistake: Treating forecast as wishful thinking ("what we hope happens")
Correct approach: Forecast is your best current estimate given all known information.
If forecast diverges from budget by > 15%, the budget is wrong.
Reforecast and communicate to board.
```
**Rolling forecast (recommended for startups):**
```
Always have a 12-month forward model.
Update it monthly with actuals replacing the first month.
The forecast should always reflect your current operational reality, not your hope.
```
---
## Key Formulas Reference
```python
# ARR and growth
ARR_growth_yoy = (ending_ARR - beginning_ARR) / beginning_ARR
# Net Dollar Retention
NDR = (beginning_MRR + expansion_MRR - churn_MRR - contraction_MRR) / beginning_MRR
# Burn Multiple
burn_multiple = net_cash_burn / net_new_ARR
# Rule of 40
rule_of_40 = revenue_growth_pct + ebitda_margin_pct
# LTV (SaaS)
LTV = (ARPA * gross_margin_pct) / monthly_churn_rate
# CAC Payback (months)
cac_payback = CAC / (ARPA * gross_margin_pct)
# Magic Number (sales efficiency)
magic_number = (net_new_ARR * 4) / prior_quarter_S_and_M_spend
# Gross margin
gross_margin = (revenue - COGS) / revenue
# Quick Ratio (growth efficiency)
quick_ratio = (new_MRR + expansion_MRR) / (churned_MRR + contraction_MRR)
# Target: > 4 for high-growth SaaS
```
FILE:references/fundraising_playbook.md
# Fundraising Playbook
From timing to close. What investors actually look for, how valuation works, and the term sheet clauses that matter.
---
## 1. When to Raise
**Optimal timing:**
```
Target: 18-24 months runway post-close
Minimum: 12 months runway post-close (leaves no buffer for slip)
Start process when: 9-12 months runway remaining
→ 3-6 months for process (typically 4-5 months for Series A/B)
→ Leaves 3-6 months buffer if process drags
Never start when: < 6 months runway
→ You're negotiating from desperation
→ Investors can smell it
→ Terms get worse, or you don't close at all
```
**Rule:** Your leverage is maximum when you don't *need* to raise. Raise from a position of momentum, not necessity.
---
## 2. What Investors Look For at Each Stage
### Pre-seed
- Team (are these people credible for this problem?)
- Problem clarity (is the problem real and meaningful?)
- Early signal (any customers paying, waitlist, prototype)
- Market size (worth building a VC-scale company?)
**Typical ask:** $500K–$2M | **Typical valuation:** $3M–$10M pre-money
### Seed
- Product-market signal (customers using and paying)
- Founding team with domain expertise
- ARR: $100K–$1M (or strong usage for PLG)
- Clear hypothesis for what Series A looks like
**Typical ask:** $2M–$5M | **Typical valuation:** $8M–$20M pre-money
### Series A
Investors are buying a *repeatable sales motion*. Not just customers — a machine.
**What they need to see:**
- ARR: $1M–$5M growing > 100% YoY
- LTV:CAC > 2.5x (and improving)
- Net Dollar Retention > 100%
- CAC Payback < 18 months
- Gross margin > 65%
- At least 5-10 reference customers (not just lighthouse)
- Sales motion that converts without the founder closing every deal
**Typical ask:** $8M–$15M | **Typical valuation:** $25M–$60M pre-money
### Series B
Investors are buying *scalable go-to-market*. Can you pour fuel on the fire?
**What they need to see:**
- ARR: $5M–$20M growing > 100% YoY
- LTV:CAC > 3x, CAC Payback < 18 months
- Sales capacity model (hiring plan → pipeline → revenue)
- NDR > 110% (expansion motion working)
- Some proof of market expansion (new segments, geographies, use cases)
- Path to category leadership
**Typical ask:** $15M–$40M | **Typical valuation:** $60M–$200M pre-money
### Series C and Beyond
Investors are buying *market leadership* and *path to profitability*.
**What they need to see:**
- ARR: $20M+ (often $30-50M for credible Series C)
- Rule of 40 > 40 (or credible path)
- Gross margin > 70%
- NDR > 115%
- Evidence of market leadership (brand, win rates, analyst mentions)
- Clear path to $100M+ ARR
---
## 3. Valuation Methods
### Revenue Multiples (Primary Method for SaaS)
```
Pre-money Valuation = ARR × Revenue Multiple
Revenue multiple benchmarks (2024-2025):
> 100% YoY growth: 8x–15x ARR
50-100% YoY growth: 4x–8x ARR
20-50% YoY growth: 2x–4x ARR
< 20% YoY growth: 1x–2x ARR
Adjustments:
NDR > 120%: +1x–2x premium
Gross margin > 75%: +0.5x–1x premium
Burn multiple < 1x: +0.5x–1x premium
Capital efficient: Investors pay up for efficiency
Declining growth: Compress multiple aggressively
```
### The Investor's Math (Know This)
Every VC has a required return. Work backwards from their constraints:
```
Investor targets: 3x fund return
Fund size: $200M, check size: $15M (initial), $25M (with follow-on)
Ownership at exit needed: 15%
At 15% ownership: needs $25M / 15% = $167M post-money valuation
Exit needed to return 3x on that check: $25M × 10 = $250M company value
(10x because most deals fail, winners must carry the fund)
Implication: If you think you'll exit for $150M, that VC will pass or price you accordingly.
```
This is why Series A investors rarely lead rounds where they can't see a $300M+ exit path. It's not about your business being bad — it's about fund math.
### Comparable Company Analysis
For later stages (Series B+):
```
1. Find 5-10 comparable public SaaS companies
2. Calculate their EV/NTM Revenue multiples (use latest data)
3. Apply a private market discount (typically 20-40% vs public comps)
4. Adjust for your growth rate relative to comps
Example (2024):
Public SaaS comps: 6x NTM Revenue (median)
Private discount: 30%
Adjusted: ~4.2x
Your NTM Revenue: $8M
Implied valuation: ~$33M pre-money
```
### DCF (Late Stage Only)
DCF is unreliable for early-stage startups (terminal value dominates, growth rate assumptions are fantasy). Use it as a sanity check at Series C+, not as the primary valuation method.
---
## 4. Term Sheet Breakdown
### Liquidation Preference (Most Important Economic Term)
This determines who gets paid first in an exit — and how much.
```
1x Non-Participating Preferred (BEST for founders):
Investor gets 1x money back OR converts to common (their choice).
At acquisition: investor takes larger of {1x invested} or {% ownership × proceeds}
Example: $10M invested, exits at $100M, owns 20%
Option A: $10M (1x)
Option B: $20M (20% of $100M)
Investor takes $20M. Founders split $80M.
1x Participating Preferred (WORSE for founders):
Investor gets 1x money back AND participates in remaining proceeds.
Example: same scenario
$10M (1x) + 20% of remaining $90M = $10M + $18M = $28M
Founders split $72M instead of $80M
Cost to founders: $8M (10% of exit value)
2x Participating (RED FLAG):
Investor gets 2x back AND participates.
Only accept under duress. Push hard against this.
Full Ratchet Anti-Dilution (AVOID):
Down-round triggers full repricing of investor shares to new (lower) price.
Founders get massively diluted. Never accept if alternatives exist.
```
### Anti-Dilution Protection
```
Broad-based weighted average (standard):
Adjusts investor conversion price based on all dilutive securities.
Most founder-friendly anti-dilution. Accept this.
Narrow-based weighted average (slightly worse):
Same mechanism but uses smaller denominator.
Gives investors slightly more protection. Usually acceptable.
Full ratchet (avoid):
Price drops to whatever the new round prices at.
Devastating in down rounds. Fight this.
```
### Pro-Rata Rights
```
Standard pro-rata: Investor can maintain their % ownership in future rounds.
Reasonable. Accept for major investors.
Super pro-rata: Investor can increase their % in future rounds.
Caps your ability to bring in new lead investors.
Avoid unless the investor is exceptional and you want them in future rounds.
Major investor threshold: Typically investors with > $500K–$1M check get pro-rata.
Don't give pro-rata to every small check — clogs future rounds.
```
### Board Composition
```
Seed (3 members): 2 founders, 1 lead investor
Series A (5 members): 2 founders, 2 investors, 1 independent
Series B (5-7 seats): Watch for investor majority — negotiate hard
Rule: Founders should retain majority through Series A.
Independent director should be your choice, not investor's.
Never accept investor majority before Series C.
Board observer rights: Common for smaller investors. No vote but present in meetings.
Limit to 1-2 observers or meetings become unwieldy.
```
### Other Terms That Matter
```
Drag-along: Majority can force minority shareholders to vote for acquisition.
Standard and reasonable. Check what threshold triggers drag.
Information rights: Investors get financial statements.
Standard. Monthly for major investors, quarterly for others.
Redemption rights: Investors can force buyback after X years.
Push to remove or add carve-outs for insufficient funds.
No-shop clause: You can't shop the term sheet to other investors.
Standard (14-30 days). Reasonable.
Exclusivity: Stronger version of no-shop. Sometimes includes no other fundraise discussions.
Acceptable for 30 days; push back on > 45 days.
```
---
## 5. Cap Table Management
### Dilution Planning Model
Run this before every round. Know your number before walking into any negotiation.
```
Pre-Seed Post-Seed Post-A Post-B Post-C
Founder A 45.0% 36.0% 26.5% 21.2% 18.7%
Founder B 45.0% 36.0% 26.5% 21.2% 18.7%
Angel 1 5.0% 4.0% 2.9% 2.4% 2.1%
Angel 2 5.0% 4.0% 2.9% 2.4% 2.1%
Seed Fund - 12.0% 8.8% 7.1% 6.2%
Option Pool - 8.0% 12.0% 10.0% 8.0%
Series A - - 20.4% 16.3% 14.4%
Series B - - - 19.5% 17.2%
Series C - - - - 12.6%
Round size / pre-money:
Pre-Seed: $500K / $9M pre = 5% dilution
Seed: $2M / $8M pre = 20% dilution (includes 8% pool)
Series A: $10M / $38M pre = 20.8% dilution (pool refresh to 12%)
Series B: $20M / $80M pre = 20% dilution
Series C: $30M / $170M pre = 15% dilution
```
**Option pool shuffle:** Investors often require you to create/expand the option pool *before* the round closes, which dilutes existing shareholders (not the incoming investor). Model this explicitly — a 20% round with a 5% pool expansion is really 24%+ dilution to founders.
### Cap Table Hygiene
```
Tools: Carta, Pulley, Capshare (all acceptable)
Never: Track cap table in a spreadsheet past seed stage. Errors compound.
Keep it clean:
- Repurchase departed co-founder shares immediately (don't let unvested shares linger)
- Convert SAFEs to equity cleanly at each priced round
- Document every grant with a board resolution
- Cliff + vesting for ALL employees and founders (standard: 1-year cliff, 4-year vest)
- 409A valuation required before every option grant (IRS requirement)
```
---
## 6. Data Room Preparation
### Core Documents (Required)
```
Financial:
□ 3 years historical financials (or all history if < 3 years)
□ Monthly P&L and cash flow (last 24 months)
□ Current financial model (18-24 months forward)
□ Budget vs actual (last 4 quarters)
□ Cap table (fully diluted, with all SAFEs/convertibles modeled)
□ Bank statements (last 3-6 months)
Legal:
□ Certificate of incorporation + all amendments
□ All prior financing documents (SAFEs, convertible notes, stock purchase agreements)
□ Cap table (Carta/Pulley export)
□ IP assignment agreements (all founders and employees)
□ Material contracts (top 10 customers, key vendors)
□ Employee list (titles, start dates, salaries, equity grants)
Product & Business:
□ Product demo / walkthrough video
□ Architecture overview (for technical investors)
□ Customer case studies (3-5 named references)
□ NPS / CSAT data
□ Competitive landscape analysis
Metrics:
□ MRR/ARR by month (all history)
□ Cohort retention chart
□ CAC by channel
□ LTV by cohort
□ NPS trend
```
### What Investors Actually Check First
In order of typical priority during due diligence:
1. **Cap table** — Is it clean? Any concerning structures?
2. **Cohort retention** — Is churn improving or deteriorating?
3. **Revenue quality** — What % is recurring? Any one-time or non-recurring?
4. **Top 10 customers** — Concentration risk? Any logos at risk?
5. **Bank statements** — Does cash match what was reported?
6. **IP assignments** — Does the company own its IP? (Founders who didn't assign IP kill deals)
### Red Flags That Kill Deals
- Missing IP assignment agreements for founders (most common deal killer at early stage)
- Cap table with > 20 angels/small investors (messy, hard to get consent for future rounds)
- Customer concentration > 30% in single customer without explanation
- Revenue recognition issues (booking ARR on contracts that allow easy cancellation)
- Cohort data that gets worse in later cohorts
- Bank balance doesn't match reported cash position
---
## 7. Investor Communication Cadence
### During Fundraise
```
Week 1-2: Warm intro sourcing, LP/network mapping
Week 3-6: First meetings (aim for 20-30 first meetings)
Week 7-10: Partner meetings, deep dives, due diligence
Week 11-14: Term sheets, negotiation
Week 15-18: Legal, closing
```
**Parallel process is essential.** Never negotiate with one investor at a time. Competition is your leverage.
### Post-Close: Investor Updates
Monthly investor update (send within 10 days of month-end):
```
Subject: [Company] Monthly Update — [Month Year]
Highlights (3 bullets max):
• [Biggest win]
• [Biggest learning/challenge]
• [What we're focused on next month]
Metrics:
ARR: $X (+X% MoM)
Net new ARR: $X
Gross margin: X%
Cash: $X (X months runway)
Headcount: X
Asks (be specific):
• Looking for intro to [persona/company] for [specific reason]
• Need advisor with experience in [specific area]
• [Other concrete ask]
```
**Why this matters:** Investors who are informed and engaged are better positioned to help when you need it. The investor who hasn't heard from you in 6 months is less likely to write a bridge check or make a warm intro when you ask.
---
## Key Formulas
```python
# Post-money valuation
post_money = pre_money + investment_amount
# Investor ownership %
ownership_pct = investment_amount / post_money
# Dilution to existing shareholders
dilution = investment_amount / post_money # as a fraction
# New shares issued
new_shares = (investment_amount / post_money) * total_post_shares
# equivalent: new_shares = pre_money_shares * (investment_amount / pre_money)
# Option pool expansion impact (pool shuffle)
# Creating X% option pool pre-close dilutes founders:
pool_shares_needed = target_pct * (pre_shares + new_round_shares + pool_shares_needed)
# Solve: pool_shares_needed = target_pct * (pre_shares + new_round_shares) / (1 - target_pct)
# LTV:CAC ratio
ltv_cac = ltv / cac # target: > 3x
# CAC payback (months)
payback_months = cac / (arpa * gross_margin_pct)
```
FILE:scripts/burn_rate_calculator.py
#!/usr/bin/env python3
"""
Burn Rate & Runway Calculator
==============================
Models startup runway across base/bull/bear scenarios, incorporating
a hiring plan and revenue trajectory. Outputs months of runway,
cash-out dates, and decision trigger points.
Usage:
python burn_rate_calculator.py
python burn_rate_calculator.py --csv # export to CSV
Stdlib only. No dependencies.
"""
import argparse
import csv
import io
import sys
from dataclasses import dataclass, field
from datetime import date, timedelta
from typing import Optional
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
@dataclass
class HiringEntry:
"""A planned hire."""
month: int # months from model start (1-indexed)
role: str
department: str # "sales", "engineering", "cs", "ga"
annual_salary: float
benefits_pct: float = 0.22 # benefits as % of salary
recruiting_cost: float = 0.0 # one-time recruiting fee
@dataclass
class RevenueEntry:
"""Monthly revenue data point (historical or projected)."""
month: int
mrr: float # monthly recurring revenue
one_time: float = 0.0
@dataclass
class ModelConfig:
"""Master configuration for a runway scenario."""
name: str
starting_cash: float
starting_mrr: float
starting_headcount: int
avg_loaded_salary: float # average fully-loaded salary per current employee
base_non_headcount_opex: float # monthly non-headcount costs (infra, tools, etc.)
gross_margin_pct: float # 0.0–1.0
mrr_growth_rate: float # monthly MoM growth rate, 0.0–1.0
hiring_plan: list[HiringEntry] = field(default_factory=list)
model_months: int = 24
start_date: Optional[date] = None
@dataclass
class MonthResult:
"""Single month output."""
month: int
label: str # e.g. "Month 1 (Apr 2025)"
mrr: float
gross_profit: float
headcount: int
headcount_cost: float # total loaded headcount cost this month
other_opex: float
gross_burn: float
net_burn: float
cash_start: float
cash_end: float
runway_months: float # projected runway from this month
cumulative_new_arr: float # for burn multiple
# ---------------------------------------------------------------------------
# Core calculator
# ---------------------------------------------------------------------------
class RunwayCalculator:
def __init__(self, config: ModelConfig):
self.cfg = config
def run(self) -> list[MonthResult]:
cfg = self.cfg
results = []
# Build headcount schedule: month -> list of new hires starting that month
hire_by_month: dict[int, list[HiringEntry]] = {}
for h in cfg.hiring_plan:
hire_by_month.setdefault(h.month, []).append(h)
# Track existing employees
active_employees: list[dict] = []
for _ in range(cfg.starting_headcount):
active_employees.append({
"monthly_loaded": cfg.avg_loaded_salary / 12 * 1.0,
"start_month": 0,
})
cash = cfg.starting_cash
mrr = cfg.starting_mrr
cumulative_new_arr = 0.0
starting_mrr = cfg.starting_mrr
for m in range(1, cfg.model_months + 1):
# Process new hires this month
one_time_recruiting = 0.0
if m in hire_by_month:
for hire in hire_by_month[m]:
monthly_loaded = (
hire.annual_salary * (1 + hire.benefits_pct) / 12
)
active_employees.append({
"monthly_loaded": monthly_loaded,
"start_month": m,
})
one_time_recruiting += hire.recruiting_cost
# Revenue this month
mrr = mrr * (1 + cfg.mrr_growth_rate)
gross_profit = mrr * cfg.gross_margin_pct
# Headcount cost
headcount_cost = sum(e["monthly_loaded"] for e in active_employees)
headcount_cost += one_time_recruiting
# Other opex (infra, SaaS tools, office, etc.)
other_opex = cfg.base_non_headcount_opex
# Burn
gross_burn = headcount_cost + other_opex
net_burn = gross_burn - gross_profit
# Cash
cash_start = cash
cash = cash - net_burn
cash_end = cash
# Projected runway from this month (using current net burn rate)
runway = cash_end / net_burn if net_burn > 0 else float("inf")
# Cumulative new ARR (for burn multiple calc)
new_mrr_added = mrr - starting_mrr if m == 1 else mrr - results[-1].mrr
cumulative_new_arr += new_mrr_added * 12
# Label
if cfg.start_date:
month_date = date(
cfg.start_date.year,
cfg.start_date.month,
1,
) + timedelta(days=32 * (m - 1))
month_date = month_date.replace(day=1)
label = f"Month {m:02d} ({month_date.strftime('%b %Y')})"
else:
label = f"Month {m:02d}"
results.append(MonthResult(
month=m,
label=label,
mrr=mrr,
gross_profit=gross_profit,
headcount=len(active_employees),
headcount_cost=headcount_cost,
other_opex=other_opex,
gross_burn=gross_burn,
net_burn=net_burn,
cash_start=cash_start,
cash_end=cash_end,
runway_months=runway,
cumulative_new_arr=cumulative_new_arr,
))
# Stop if cash runs out
if cash_end <= 0:
break
return results
def cash_out_date(self, results: list[MonthResult]) -> Optional[str]:
"""Return the label of the month cash runs out, or None if model survives."""
for r in results:
if r.cash_end <= 0:
return r.label
return None
def burn_multiple(self, results: list[MonthResult]) -> float:
"""Burn multiple = total net burn / total net new ARR over model period."""
total_net_burn = sum(r.net_burn for r in results if r.net_burn > 0)
first_mrr = results[0].mrr / (1 + self.cfg.mrr_growth_rate) # starting mrr
total_new_arr = (results[-1].mrr - first_mrr) * 12
if total_new_arr <= 0:
return float("inf")
return total_net_burn / total_new_arr
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt_k(value: float) -> str:
"""Format as $Xk or $X.XM."""
if abs(value) >= 1_000_000:
return f".2fM"
if abs(value) >= 1_000:
return f".0fK"
return f".0f"
def print_summary(name: str, results: list[MonthResult], calc: RunwayCalculator) -> None:
cash_out = calc.cash_out_date(results)
bm = calc.burn_multiple(results)
last = results[-1]
first = results[0]
print(f"\n{'='*60}")
print(f" SCENARIO: {name}")
print(f"{'='*60}")
print(f" Months modeled: {len(results)}")
print(f" Cash out: {cash_out or 'Does not run out in model period'}")
print(f" Ending cash: {fmt_k(last.cash_end)}")
print(f" Final runway: {last.runway_months:.1f} months")
print(f" Starting MRR: {fmt_k(first.mrr)}")
print(f" Ending MRR: {fmt_k(last.mrr)}")
print(f" Ending headcount: {last.headcount}")
print(f" Burn multiple: {bm:.2f}x")
print(f" Avg net burn: {fmt_k(sum(r.net_burn for r in results)/len(results))}/mo")
# Decision triggers
print(f"\n Decision Triggers:")
triggers = {9: "⚠️ START FUNDRAISE", 6: "🔴 COST REDUCTION PLAN", 4: "🚨 EXECUTE CUTS / BRIDGE"}
shown = set()
for r in results:
for threshold, label in triggers.items():
if r.runway_months <= threshold and threshold not in shown:
print(f" {r.label}: {label} (runway = {r.runway_months:.1f} mo)")
shown.add(threshold)
def print_monthly_table(results: list[MonthResult], max_rows: int = 24) -> None:
header = f"{'Month':<22} {'MRR':>10} {'Hdct':>6} {'Net Burn':>12} {'Cash':>12} {'Runway':>8}"
print(f"\n{header}")
print("-" * len(header))
for r in results[:max_rows]:
runway_str = f"{r.runway_months:.1f}mo" if r.runway_months != float("inf") else "∞"
print(
f"{r.label:<22} "
f"{fmt_k(r.mrr):>10} "
f"{r.headcount:>6} "
f"{fmt_k(r.net_burn):>12} "
f"{fmt_k(r.cash_end):>12} "
f"{runway_str:>8}"
)
def export_csv(scenarios: list[tuple[str, list[MonthResult]]]) -> str:
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow([
"Scenario", "Month", "Label", "MRR", "Gross Profit", "Headcount",
"Headcount Cost", "Other Opex", "Gross Burn", "Net Burn",
"Cash Start", "Cash End", "Runway Months"
])
for name, results in scenarios:
for r in results:
writer.writerow([
name, r.month, r.label,
round(r.mrr, 2), round(r.gross_profit, 2), r.headcount,
round(r.headcount_cost, 2), round(r.other_opex, 2),
round(r.gross_burn, 2), round(r.net_burn, 2),
round(r.cash_start, 2), round(r.cash_end, 2),
round(r.runway_months, 2),
])
return buf.getvalue()
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
def make_sample_configs() -> list[ModelConfig]:
"""
Sample company: Series A SaaS startup
- $3M cash on hand (post Series A)
- $125K MRR (~$1.5M ARR)
- 18 employees, $150K avg salary
- $80K/mo non-headcount opex (infra, tools, office)
- 72% gross margin
"""
common_kwargs = dict(
starting_cash=3_000_000,
starting_mrr=125_000,
starting_headcount=18,
avg_loaded_salary=150_000,
base_non_headcount_opex=80_000,
gross_margin_pct=0.72,
model_months=24,
start_date=date(2025, 1, 1),
)
# Base: 10% MoM growth, moderate hiring
base_hiring = [
HiringEntry(month=2, role="AE #1", department="sales", annual_salary=120_000, recruiting_cost=18_000),
HiringEntry(month=3, role="Senior SWE #1", department="engineering", annual_salary=160_000, recruiting_cost=24_000),
HiringEntry(month=5, role="SDR #1", department="sales", annual_salary=80_000, recruiting_cost=12_000),
HiringEntry(month=6, role="CSM #1", department="cs", annual_salary=90_000, recruiting_cost=13_500),
HiringEntry(month=8, role="AE #2", department="sales", annual_salary=120_000, recruiting_cost=18_000),
HiringEntry(month=9, role="Senior SWE #2", department="engineering", annual_salary=165_000, recruiting_cost=24_750),
HiringEntry(month=12, role="Controller", department="ga", annual_salary=130_000, recruiting_cost=19_500),
HiringEntry(month=14, role="AE #3", department="sales", annual_salary=125_000, recruiting_cost=18_750),
HiringEntry(month=15, role="ML Engineer", department="engineering", annual_salary=175_000, recruiting_cost=26_250),
HiringEntry(month=18, role="AE #4", department="sales", annual_salary=125_000, recruiting_cost=18_750),
]
# Bull: 15% MoM growth, full hiring plan
bull_hiring = base_hiring + [
HiringEntry(month=4, role="Marketing Manager", department="sales", annual_salary=110_000, recruiting_cost=16_500),
HiringEntry(month=7, role="Senior SWE #3", department="engineering", annual_salary=165_000, recruiting_cost=24_750),
HiringEntry(month=10, role="AE #5", department="sales", annual_salary=125_000, recruiting_cost=18_750),
HiringEntry(month=13, role="DevOps Engineer", department="engineering", annual_salary=150_000, recruiting_cost=22_500),
HiringEntry(month=16, role="AE #6", department="sales", annual_salary=125_000, recruiting_cost=18_750),
]
# Bear: 5% MoM growth, hiring freeze after month 3
bear_hiring = [
HiringEntry(month=2, role="AE #1", department="sales", annual_salary=120_000, recruiting_cost=18_000),
HiringEntry(month=3, role="Senior SWE #1", department="engineering", annual_salary=160_000, recruiting_cost=24_000),
]
return [
ModelConfig(name="BULL (15% MoM, full hiring)", mrr_growth_rate=0.15, hiring_plan=bull_hiring, **common_kwargs),
ModelConfig(name="BASE (10% MoM, planned hiring)", mrr_growth_rate=0.10, hiring_plan=base_hiring, **common_kwargs),
ModelConfig(name="BEAR ( 5% MoM, hiring freeze M3+)", mrr_growth_rate=0.05, hiring_plan=bear_hiring, **common_kwargs),
ModelConfig(name="DISTRESS (0% growth, freeze now)", mrr_growth_rate=0.00, hiring_plan=[], **common_kwargs),
]
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description="Startup Burn Rate & Runway Calculator")
parser.add_argument("--csv", action="store_true", help="Export full monthly data as CSV to stdout")
parser.add_argument("--scenario", choices=["bull", "base", "bear", "distress", "all"], default="all")
args = parser.parse_args()
configs = make_sample_configs()
if args.scenario != "all":
configs = [c for c in configs if args.scenario.upper() in c.name.upper()]
all_results: list[tuple[str, list[MonthResult]]] = []
print("\n" + "="*60)
print(" BURN RATE & RUNWAY CALCULATOR")
print(" Sample Company: Series A SaaS Startup")
print(" Starting cash: $3M | Starting MRR: $125K | 18 employees")
print("="*60)
for cfg in configs:
calc = RunwayCalculator(cfg)
results = calc.run()
all_results.append((cfg.name, results))
print_summary(cfg.name, results, calc)
print_monthly_table(results)
# Comparison summary
print("\n" + "="*60)
print(" SCENARIO COMPARISON")
print("="*60)
print(f" {'Scenario':<40} {'Runway':>8} {'Cash Out':<30} {'Burn Mult':>10}")
print(" " + "-"*88)
for cfg, (name, results) in zip(configs, all_results):
calc = RunwayCalculator(cfg)
cash_out = calc.cash_out_date(results) or "Survives model period"
bm = calc.burn_multiple(results)
final_runway = results[-1].runway_months
runway_str = f"{final_runway:.1f}mo" if final_runway != float("inf") else "∞"
bm_str = f"{bm:.2f}x" if bm != float("inf") else "∞"
print(f" {name:<40} {runway_str:>8} {cash_out:<30} {bm_str:>10}")
print("\n Decision Trigger Reference:")
print(" 9 months runway → Start fundraise process")
print(" 6 months runway → Begin cost reduction planning")
print(" 4 months runway → Execute cuts; explore bridge financing")
print(" 3 months runway → Emergency plan only")
if args.csv:
print("\n\n--- CSV EXPORT ---\n")
sys.stdout.write(export_csv(all_results))
if __name__ == "__main__":
main()
FILE:scripts/fundraising_model.py
#!/usr/bin/env python3
"""
Fundraising Model
==================
Cap table management, dilution modeling, and multi-round scenario planning.
Know exactly what you're giving up before you walk into any negotiation.
Covers:
- Cap table state at each round
- Dilution per shareholder per round
- Option pool shuffle impact
- Multi-round projections (Seed → A → B → C)
- Return scenarios at different exit valuations
Usage:
python fundraising_model.py
python fundraising_model.py --exit 150 # model at $150M exit
python fundraising_model.py --csv
Stdlib only. No dependencies.
"""
import argparse
import csv
import io
import sys
from dataclasses import dataclass, field
from typing import Optional
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
@dataclass
class Shareholder:
"""A shareholder in the cap table."""
name: str
share_class: str # "common", "preferred", "option"
shares: float
invested: float = 0.0 # total cash invested
is_option_pool: bool = False
@dataclass
class RoundConfig:
"""Configuration for a financing round."""
name: str # e.g. "Series A"
pre_money_valuation: float
investment_amount: float
new_option_pool_pct: float = 0.0 # % of POST-money to allocate to new options
option_pool_pre_round: bool = True # True = pool created before round (dilutes founders)
lead_investor_name: str = "New Investor"
share_price_override: Optional[float] = None # if None, computed from valuation
@dataclass
class CapTableEntry:
"""A row in the cap table at a point in time."""
name: str
share_class: str
shares: float
pct_ownership: float
invested: float
is_option_pool: bool = False
@dataclass
class RoundResult:
"""Snapshot of cap table after a round closes."""
round_name: str
pre_money_valuation: float
investment_amount: float
post_money_valuation: float
price_per_share: float
new_shares_issued: float
option_pool_shares_created: float
total_shares: float
cap_table: list[CapTableEntry]
@dataclass
class ExitAnalysis:
"""Proceeds to each shareholder at an exit."""
exit_valuation: float
shareholder: str
shares: float
ownership_pct: float
proceeds_common: float # if all preferred converts to common
invested: float
moic: float # multiple on invested capital (for investors)
# ---------------------------------------------------------------------------
# Core cap table engine
# ---------------------------------------------------------------------------
class CapTable:
"""Manages a cap table through multiple rounds."""
def __init__(self):
self.shareholders: list[Shareholder] = []
self._total_shares: float = 0.0
def add_shareholder(self, sh: Shareholder) -> None:
self.shareholders.append(sh)
self._total_shares += sh.shares
def total_shares(self) -> float:
return sum(s.shares for s in self.shareholders)
def snapshot(self, label: str = "") -> list[CapTableEntry]:
total = self.total_shares()
return [
CapTableEntry(
name=s.name,
share_class=s.share_class,
shares=s.shares,
pct_ownership=s.shares / total if total > 0 else 0,
invested=s.invested,
is_option_pool=s.is_option_pool,
)
for s in self.shareholders
]
def execute_round(self, config: RoundConfig) -> RoundResult:
"""
Execute a financing round:
1. (Optional) Create option pool pre-round (dilutes existing shareholders)
2. Issue new shares to investor at round price
Returns a RoundResult with full cap table snapshot.
"""
current_total = self.total_shares()
# Step 1: Option pool shuffle (if pre-round)
option_pool_shares_created = 0.0
if config.new_option_pool_pct > 0 and config.option_pool_pre_round:
# Target: post-round option pool = new_option_pool_pct of total post-money shares
# Solve: pool_shares / (current_total + pool_shares + new_investor_shares) = target_pct
# This requires iteration because new_investor_shares also depends on pool_shares
# Simplification: create pool based on post-round total (slightly approximated)
target_post_round_pct = config.new_option_pool_pct
post_money = config.pre_money_valuation + config.investment_amount
# Estimate shares per dollar (price per share)
price_per_share = config.pre_money_valuation / current_total
new_investor_shares_estimate = config.investment_amount / price_per_share
# Pool shares needed so that pool / total_post = target_pct
total_post_estimate = current_total + new_investor_shares_estimate
pool_shares_needed = (target_post_round_pct * total_post_estimate) / (1 - target_post_round_pct)
# Check if existing pool is sufficient
existing_pool = next(
(s.shares for s in self.shareholders if s.is_option_pool), 0
)
additional_pool_needed = max(0, pool_shares_needed - existing_pool)
if additional_pool_needed > 0:
option_pool_shares_created = additional_pool_needed
# Add to existing pool or create new
pool_sh = next((s for s in self.shareholders if s.is_option_pool), None)
if pool_sh:
pool_sh.shares += additional_pool_needed
else:
self.shareholders.append(Shareholder(
name="Option Pool",
share_class="option",
shares=additional_pool_needed,
is_option_pool=True,
))
# Step 2: Price per share (after pool creation)
current_total_post_pool = self.total_shares()
if config.share_price_override:
price_per_share = config.share_price_override
else:
price_per_share = config.pre_money_valuation / current_total_post_pool
# Step 3: New shares for investor
new_shares = config.investment_amount / price_per_share
# Step 4: Add investor to cap table
self.shareholders.append(Shareholder(
name=config.lead_investor_name,
share_class="preferred",
shares=new_shares,
invested=config.investment_amount,
))
post_money = config.pre_money_valuation + config.investment_amount
total_post = self.total_shares()
return RoundResult(
round_name=config.name,
pre_money_valuation=config.pre_money_valuation,
investment_amount=config.investment_amount,
post_money_valuation=post_money,
price_per_share=price_per_share,
new_shares_issued=new_shares,
option_pool_shares_created=option_pool_shares_created,
total_shares=total_post,
cap_table=self.snapshot(),
)
def analyze_exit(self, exit_valuation: float) -> list[ExitAnalysis]:
"""
Simple exit analysis: all preferred converts to common, proceeds split pro-rata.
(Does not model liquidation preferences — see fundraising_playbook.md for that.)
"""
total = self.total_shares()
price_per_share = exit_valuation / total
results = []
for s in self.shareholders:
if s.is_option_pool:
continue # unissued options don't receive proceeds
proceeds = s.shares * price_per_share
moic = proceeds / s.invested if s.invested > 0 else 0.0
results.append(ExitAnalysis(
exit_valuation=exit_valuation,
shareholder=s.name,
shares=s.shares,
ownership_pct=s.shares / total,
proceeds_common=proceeds,
invested=s.invested,
moic=moic,
))
return sorted(results, key=lambda x: x.proceeds_common, reverse=True)
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt(value: float, prefix: str = "$") -> str:
if value == float("inf"):
return "∞"
if abs(value) >= 1_000_000:
return f"{prefix}{value/1_000_000:.2f}M"
if abs(value) >= 1_000:
return f"{prefix}{value/1_000:.0f}K"
return f"{prefix}{value:.2f}"
def print_round_result(result: RoundResult, prev_cap_table: Optional[list[CapTableEntry]] = None) -> None:
print(f"\n{'='*70}")
print(f" {result.round_name.upper()}")
print(f"{'='*70}")
print(f" Pre-money valuation: {fmt(result.pre_money_valuation)}")
print(f" Investment: {fmt(result.investment_amount)}")
print(f" Post-money valuation: {fmt(result.post_money_valuation)}")
print(f" Price per share: {fmt(result.price_per_share, '$')}")
print(f" New shares issued: {result.new_shares_issued:,.0f}")
if result.option_pool_shares_created > 0:
print(f" Option pool created: {result.option_pool_shares_created:,.0f} shares")
print(f" ⚠️ Pool created pre-round: dilutes existing shareholders, not new investor")
print(f" Total shares post: {result.total_shares:,.0f}")
print(f"\n {'Shareholder':<22} {'Shares':>12} {'Ownership':>10} {'Invested':>10} {'Δ Ownership':>12}")
print(" " + "-"*68)
prev_map = {e.name: e.pct_ownership for e in prev_cap_table} if prev_cap_table else {}
for entry in result.cap_table:
delta = ""
if entry.name in prev_map:
change = (entry.pct_ownership - prev_map[entry.name]) * 100
delta = f"{change:+.1f}pp"
elif not entry.is_option_pool:
delta = "new"
invested_str = fmt(entry.invested) if entry.invested > 0 else "-"
print(
f" {entry.name:<22} {entry.shares:>12,.0f} "
f"{entry.pct_ownership*100:>9.2f}% {invested_str:>10} {delta:>12}"
)
def print_exit_analysis(results: list[ExitAnalysis], exit_valuation: float) -> None:
print(f"\n{'='*70}")
print(f" EXIT ANALYSIS @ {fmt(exit_valuation)} (all preferred converts to common)")
print(f"{'='*70}")
print(f"\n {'Shareholder':<22} {'Ownership':>10} {'Proceeds':>12} {'Invested':>10} {'MOIC':>8}")
print(" " + "-"*65)
for r in results:
moic_str = f"{r.moic:.1f}x" if r.moic > 0 else "n/a"
invested_str = fmt(r.invested) if r.invested > 0 else "-"
print(
f" {r.shareholder:<22} {r.ownership_pct*100:>9.2f}% "
f"{fmt(r.proceeds_common):>12} {invested_str:>10} {moic_str:>8}"
)
print(f"\n Note: Does not model liquidation preferences.")
print(f" Participating preferred reduces founder proceeds in most real exits.")
print(f" See references/fundraising_playbook.md for full liquidation waterfall.")
def print_dilution_summary(rounds: list[RoundResult]) -> None:
print(f"\n{'='*70}")
print(f" DILUTION SUMMARY — FOUNDER PERSPECTIVE")
print(f"{'='*70}")
# Find all founders (common shareholders who aren't investors or option pool)
founder_names = []
for entry in rounds[0].cap_table:
if entry.share_class == "common" and not entry.is_option_pool:
founder_names.append(entry.name)
if not founder_names:
print(" No common shareholders found in initial cap table.")
return
header = f" {'Round':<16}" + "".join(f" {n:<16}" for n in founder_names) + f" {'Total Inv':>12}"
print(header)
print(" " + "-" * (16 + 18 * len(founder_names) + 14))
for result in rounds:
cap_map = {e.name: e for e in result.cap_table}
total_invested = sum(e.invested for e in result.cap_table if not e.is_option_pool)
row = f" {result.round_name:<16}"
for name in founder_names:
pct = cap_map[name].pct_ownership * 100 if name in cap_map else 0
row += f" {pct:>6.2f}% "
row += f" {fmt(total_invested):>12}"
print(row)
def export_csv_rounds(rounds: list[RoundResult]) -> str:
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["Round", "Shareholder", "Share Class", "Shares", "Ownership Pct",
"Invested", "Pre Money", "Post Money", "Price Per Share"])
for r in rounds:
for entry in r.cap_table:
writer.writerow([
r.round_name, entry.name, entry.share_class,
round(entry.shares, 0), round(entry.pct_ownership * 100, 4),
round(entry.invested, 2), round(r.pre_money_valuation, 0),
round(r.post_money_valuation, 0), round(r.price_per_share, 4),
])
return buf.getvalue()
# ---------------------------------------------------------------------------
# Sample data: typical two-founder Series A/B/C startup
# ---------------------------------------------------------------------------
def build_sample_model() -> tuple[CapTable, list[RoundResult]]:
"""
Sample company:
- 2 founders, started with 10M shares each
- 1M shares for early advisor
- Raises Pre-seed → Seed → Series A → Series B → Series C
"""
cap = CapTable()
SHARES_PER_FOUNDER = 4_000_000
SHARES_ADVISOR = 200_000
# Founding state
cap.add_shareholder(Shareholder("Founder A (CEO)", "common", SHARES_PER_FOUNDER))
cap.add_shareholder(Shareholder("Founder B (CTO)", "common", SHARES_PER_FOUNDER))
cap.add_shareholder(Shareholder("Advisor", "common", SHARES_ADVISOR))
rounds: list[RoundResult] = []
prev_cap = cap.snapshot()
# Round 1: Pre-seed — $500K at $4.5M pre, 10% option pool created
r1 = cap.execute_round(RoundConfig(
name="Pre-seed",
pre_money_valuation=4_500_000,
investment_amount=500_000,
new_option_pool_pct=0.10,
option_pool_pre_round=True,
lead_investor_name="Angel Syndicate",
))
rounds.append(r1)
prev_r1 = r1.cap_table[:]
# Round 2: Seed — $2M at $9M pre, expand option pool to 12%
r2 = cap.execute_round(RoundConfig(
name="Seed",
pre_money_valuation=9_000_000,
investment_amount=2_000_000,
new_option_pool_pct=0.12,
option_pool_pre_round=True,
lead_investor_name="Seed Fund",
))
rounds.append(r2)
# Round 3: Series A — $12M at $38M pre, refresh option pool to 15%
r3 = cap.execute_round(RoundConfig(
name="Series A",
pre_money_valuation=38_000_000,
investment_amount=12_000_000,
new_option_pool_pct=0.15,
option_pool_pre_round=True,
lead_investor_name="Series A Fund",
))
rounds.append(r3)
# Round 4: Series B — $25M at $95M pre, refresh pool to 12%
r4 = cap.execute_round(RoundConfig(
name="Series B",
pre_money_valuation=95_000_000,
investment_amount=25_000_000,
new_option_pool_pct=0.12,
option_pool_pre_round=True,
lead_investor_name="Series B Fund",
))
rounds.append(r4)
# Round 5: Series C — $40M at $185M pre, refresh pool to 10%
r5 = cap.execute_round(RoundConfig(
name="Series C",
pre_money_valuation=185_000_000,
investment_amount=40_000_000,
new_option_pool_pct=0.10,
option_pool_pre_round=True,
lead_investor_name="Series C Fund",
))
rounds.append(r5)
return cap, rounds
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description="Fundraising Model — Cap Table & Dilution")
parser.add_argument("--exit", type=float, default=250.0,
help="Exit valuation in $M for return analysis (default: 250)")
parser.add_argument("--csv", action="store_true", help="Export round data as CSV to stdout")
args = parser.parse_args()
exit_valuation = args.exit * 1_000_000
print("\n" + "="*70)
print(" FUNDRAISING MODEL — CAP TABLE & DILUTION ANALYSIS")
print(" Sample Company: Two-founder SaaS startup")
print(" Pre-seed → Seed → Series A → Series B → Series C")
print("="*70)
cap, rounds = build_sample_model()
# Print each round
prev = None
for r in rounds:
print_round_result(r, prev)
prev = r.cap_table
# Dilution summary table
print_dilution_summary(rounds)
# Exit analysis at specified valuation
exit_results = cap.analyze_exit(exit_valuation)
print_exit_analysis(exit_results, exit_valuation)
# Also print at 2x and 5x for sensitivity
print("\n Exit Sensitivity — Founder A Proceeds:")
print(f" {'Exit Valuation':<20} {'Founder A %':>12} {'Founder A $':>14} {'MOIC':>8}")
print(" " + "-"*56)
for mult in [0.5, 1.0, 1.5, 2.0, 3.0, 5.0]:
val = rounds[-1].post_money_valuation * mult
ex = cap.analyze_exit(val)
founder_a = next((r for r in ex if r.shareholder == "Founder A (CEO)"), None)
if founder_a:
print(f" {fmt(val):<20} {founder_a.ownership_pct*100:>11.2f}% "
f"{fmt(founder_a.proceeds_common):>14} {'n/a':>8}")
print("\n Key Takeaways:")
final = rounds[-1].cap_table
total = sum(e.shares for e in final)
founder_a_final = next((e for e in final if e.name == "Founder A (CEO)"), None)
if founder_a_final:
print(f" Founder A final ownership: {founder_a_final.pct_ownership*100:.2f}%")
total_raised = sum(e.invested for e in final)
print(f" Total capital raised: {fmt(total_raised)}")
print(f" Total shares outstanding: {total:,.0f}")
print(f" Final post-money: {fmt(rounds[-1].post_money_valuation)}")
print("\n Run with --exit <$M> to model proceeds at different exit valuations.")
print(" Example: python fundraising_model.py --exit 500")
if args.csv:
print("\n\n--- CSV EXPORT ---\n")
sys.stdout.write(export_csv_rounds(rounds))
if __name__ == "__main__":
main()
FILE:scripts/unit_economics_analyzer.py
#!/usr/bin/env python3
"""
Unit Economics Analyzer
========================
Per-cohort LTV, per-channel CAC, payback periods, and LTV:CAC ratios.
Never blended averages — those hide what's actually happening.
Usage:
python unit_economics_analyzer.py
python unit_economics_analyzer.py --csv
Stdlib only. No dependencies.
"""
import argparse
import csv
import io
import sys
from dataclasses import dataclass, field
from typing import Optional
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
@dataclass
class CohortData:
"""
Revenue data for a group of customers acquired in the same period.
Revenue is tracked monthly: revenue[0] = month 1, revenue[1] = month 2, etc.
"""
label: str # e.g. "Q1 2024"
acquisition_period: str # human-readable label
customers_acquired: int
total_cac_spend: float # total S&M spend to acquire this cohort
monthly_revenue: list[float] # revenue per month from this cohort
gross_margin_pct: float = 0.70 # blended gross margin for this cohort
@dataclass
class ChannelData:
"""Acquisition cost and customer data for a single channel."""
channel: str
spend: float
customers_acquired: int
avg_arpa: float # average revenue per account (monthly)
gross_margin_pct: float = 0.70
avg_monthly_churn: float = 0.02 # monthly churn rate for customers from this channel
@dataclass
class UnitEconomicsResult:
"""Computed unit economics for a cohort or channel."""
label: str
customers: int
cac: float
arpa: float # average revenue per account per month
gross_margin_pct: float
monthly_churn: float
ltv: float
ltv_cac_ratio: float
payback_months: float
# Cohort-specific
m1_revenue: Optional[float] = None
m6_revenue: Optional[float] = None
m12_revenue: Optional[float] = None
m24_revenue: Optional[float] = None
m12_ltv: Optional[float] = None # realized LTV through month 12
retention_m6: Optional[float] = None # % of M1 revenue retained at M6
retention_m12: Optional[float] = None
# ---------------------------------------------------------------------------
# Calculators
# ---------------------------------------------------------------------------
def calc_ltv(arpa: float, gross_margin_pct: float, monthly_churn: float) -> float:
"""
LTV = (ARPA × Gross Margin) / Monthly Churn Rate
Assumes constant churn (simplified; cohort method is more accurate).
"""
if monthly_churn <= 0:
return float("inf")
return (arpa * gross_margin_pct) / monthly_churn
def calc_payback(cac: float, arpa: float, gross_margin_pct: float) -> float:
"""
CAC Payback (months) = CAC / (ARPA × Gross Margin)
"""
denominator = arpa * gross_margin_pct
if denominator <= 0:
return float("inf")
return cac / denominator
def analyze_cohort(cohort: CohortData) -> UnitEconomicsResult:
"""Compute full unit economics for a cohort."""
n = cohort.customers_acquired
if n == 0:
raise ValueError(f"Cohort {cohort.label}: customers_acquired cannot be 0")
cac = cohort.total_cac_spend / n
# ARPA from month 1 revenue
m1_rev = cohort.monthly_revenue[0] if cohort.monthly_revenue else 0
arpa = m1_rev / n if n > 0 else 0
# Observed monthly churn from cohort data
# Use revenue decline from M1 to M12 to estimate churn
months_available = len(cohort.monthly_revenue)
if months_available >= 12:
m12_rev = cohort.monthly_revenue[11]
# Revenue retention over 12 months: (M12/M1)^(1/11) per month on average
# Implied monthly retention rate
if m1_rev > 0 and m12_rev > 0:
monthly_retention = (m12_rev / m1_rev) ** (1 / 11)
monthly_churn = 1 - monthly_retention
else:
monthly_churn = 0.02 # default
elif months_available >= 6:
m6_rev = cohort.monthly_revenue[5]
if m1_rev > 0 and m6_rev > 0:
monthly_retention = (m6_rev / m1_rev) ** (1 / 5)
monthly_churn = 1 - monthly_retention
else:
monthly_churn = 0.02
else:
monthly_churn = 0.02 # default if < 6 months data
# Clamp to reasonable range
monthly_churn = max(0.001, min(monthly_churn, 0.30))
ltv = calc_ltv(arpa, cohort.gross_margin_pct, monthly_churn)
payback = calc_payback(cac, arpa, cohort.gross_margin_pct)
ltv_cac = ltv / cac if cac > 0 else float("inf")
# Snapshot revenues
def rev_at(month_idx: int) -> Optional[float]:
if months_available > month_idx:
return cohort.monthly_revenue[month_idx]
return None
m6 = rev_at(5)
m12 = rev_at(11)
m24 = rev_at(23)
# Realized LTV through observed months (actual gross profit)
m12_ltv = sum(cohort.monthly_revenue[:12]) * cohort.gross_margin_pct if months_available >= 12 else None
# Retention rates
ret_m6 = (m6 / m1_rev) if (m6 is not None and m1_rev > 0) else None
ret_m12 = (m12 / m1_rev) if (m12 is not None and m1_rev > 0) else None
return UnitEconomicsResult(
label=cohort.label,
customers=n,
cac=cac,
arpa=arpa,
gross_margin_pct=cohort.gross_margin_pct,
monthly_churn=monthly_churn,
ltv=ltv,
ltv_cac_ratio=ltv_cac,
payback_months=payback,
m1_revenue=m1_rev,
m6_revenue=m6,
m12_revenue=m12,
m24_revenue=m24,
m12_ltv=m12_ltv,
retention_m6=ret_m6,
retention_m12=ret_m12,
)
def analyze_channel(ch: ChannelData) -> UnitEconomicsResult:
"""Compute unit economics for an acquisition channel."""
if ch.customers_acquired == 0:
raise ValueError(f"Channel {ch.channel}: customers_acquired cannot be 0")
cac = ch.spend / ch.customers_acquired
ltv = calc_ltv(ch.avg_arpa, ch.gross_margin_pct, ch.avg_monthly_churn)
payback = calc_payback(cac, ch.avg_arpa, ch.gross_margin_pct)
ltv_cac = ltv / cac if cac > 0 else float("inf")
return UnitEconomicsResult(
label=ch.channel,
customers=ch.customers_acquired,
cac=cac,
arpa=ch.avg_arpa,
gross_margin_pct=ch.gross_margin_pct,
monthly_churn=ch.avg_monthly_churn,
ltv=ltv,
ltv_cac_ratio=ltv_cac,
payback_months=payback,
)
# ---------------------------------------------------------------------------
# Blended metrics (for comparison)
# ---------------------------------------------------------------------------
def blended_cac(channels: list[ChannelData]) -> float:
total_spend = sum(c.spend for c in channels)
total_customers = sum(c.customers_acquired for c in channels)
return total_spend / total_customers if total_customers > 0 else 0
def blended_ltv(channels: list[ChannelData]) -> float:
"""Weighted average LTV by customers acquired."""
total_customers = sum(c.customers_acquired for c in channels)
if total_customers == 0:
return 0
weighted = sum(
calc_ltv(c.avg_arpa, c.gross_margin_pct, c.avg_monthly_churn) * c.customers_acquired
for c in channels
)
return weighted / total_customers
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt(value: float, prefix: str = "$", decimals: int = 0) -> str:
if value == float("inf"):
return "∞"
if abs(value) >= 1_000_000:
return f"{prefix}{value/1_000_000:.2f}M"
if abs(value) >= 1_000:
return f"{prefix}{value/1_000:.1f}K"
return f"{prefix}{value:.{decimals}f}"
def pct(value: Optional[float]) -> str:
if value is None:
return "n/a"
return f"{value*100:.1f}%"
def rating(ltv_cac: float, payback: float) -> str:
if ltv_cac == float("inf"):
return "∞"
if ltv_cac >= 5 and payback <= 12:
return "🟢 Excellent"
if ltv_cac >= 3 and payback <= 18:
return "🟡 Good"
if ltv_cac >= 2 and payback <= 24:
return "🟠 Marginal"
return "🔴 Poor"
def print_cohort_analysis(results: list[UnitEconomicsResult]) -> None:
print("\n" + "="*80)
print(" COHORT ANALYSIS")
print("="*80)
print(f" {'Cohort':<12} {'Cust':>5} {'CAC':>8} {'ARPA/mo':>9} {'Churn/mo':>10} "
f"{'LTV':>10} {'LTV:CAC':>8} {'Payback':>9} {'Ret@M12':>8}")
print(" " + "-"*88)
for r in results:
payback_str = f"{r.payback_months:.1f}mo" if r.payback_months != float("inf") else "∞"
ltv_str = fmt(r.ltv) if r.ltv != float("inf") else "∞"
ltv_cac_str = f"{r.ltv_cac_ratio:.1f}x" if r.ltv_cac_ratio != float("inf") else "∞"
print(
f" {r.label:<12} {r.customers:>5} {fmt(r.cac):>8} {fmt(r.arpa):>9} "
f"{pct(r.monthly_churn):>10} {ltv_str:>10} {ltv_cac_str:>8} "
f"{payback_str:>9} {pct(r.retention_m12):>8}"
)
# Trend analysis
print("\n Cohort Trend (is the business getting better or worse?):")
if len(results) >= 3:
ltv_cac_values = [r.ltv_cac_ratio for r in results if r.ltv_cac_ratio != float("inf")]
cac_values = [r.cac for r in results]
churn_values = [r.monthly_churn for r in results]
if len(ltv_cac_values) >= 2:
ltv_cac_trend = "↑ Improving" if ltv_cac_values[-1] > ltv_cac_values[0] else "↓ Deteriorating"
else:
ltv_cac_trend = "n/a"
cac_trend = "↓ Decreasing (good)" if cac_values[-1] < cac_values[0] else "↑ Increasing"
churn_trend = "↓ Improving" if churn_values[-1] < churn_values[0] else "↑ Worsening"
print(f" LTV:CAC: {ltv_cac_trend}")
print(f" CAC: {cac_trend}")
print(f" Churn rate: {churn_trend}")
def print_channel_analysis(results: list[UnitEconomicsResult], channels: list[ChannelData]) -> None:
print("\n" + "="*80)
print(" CHANNEL ANALYSIS (Per-Channel vs Blended)")
print("="*80)
print(f" {'Channel':<22} {'Spend':>9} {'Cust':>5} {'CAC':>8} {'LTV':>10} {'LTV:CAC':>8} {'Payback':>9} {'Rating'}")
print(" " + "-"*90)
for r, ch in zip(results, channels):
payback_str = f"{r.payback_months:.1f}mo" if r.payback_months != float("inf") else "∞"
ltv_str = fmt(r.ltv) if r.ltv != float("inf") else "∞"
ltv_cac_str = f"{r.ltv_cac_ratio:.1f}x" if r.ltv_cac_ratio != float("inf") else "∞"
print(
f" {r.label:<22} {fmt(ch.spend):>9} {r.customers:>5} {fmt(r.cac):>8} "
f"{ltv_str:>10} {ltv_cac_str:>8} {payback_str:>9} {rating(r.ltv_cac_ratio, r.payback_months)}"
)
# Blended comparison
b_cac = blended_cac(channels)
b_ltv = blended_ltv(channels)
b_ltv_cac = b_ltv / b_cac if b_cac > 0 else 0
total_spend = sum(c.spend for c in channels)
total_customers = sum(c.customers_acquired for c in channels)
avg_payback = sum(
calc_payback(b_cac, c.avg_arpa, c.gross_margin_pct) * c.customers_acquired
for c in channels
) / total_customers
print(" " + "-"*90)
print(
f" {'BLENDED (dangerous)':<22} {fmt(total_spend):>9} {total_customers:>5} "
f"{fmt(b_cac):>8} {fmt(b_ltv):>10} {b_ltv_cac:.1f}x{'':<7} "
f"{avg_payback:.1f}mo{'':<4} {rating(b_ltv_cac, avg_payback)}"
)
print("\n ⚠️ Blended numbers hide channel-level problems. Manage channels individually.")
# Budget reallocation
print("\n Recommended Budget Reallocation:")
sorted_results = sorted(zip(results, channels), key=lambda x: x[0].ltv_cac_ratio, reverse=True)
for r, ch in sorted_results:
if r.ltv_cac_ratio >= 3:
action = "✅ Scale"
elif r.ltv_cac_ratio >= 2:
action = "🔄 Optimize"
else:
action = "❌ Cut / pause"
print(f" {ch.channel:<22} LTV:CAC = {r.ltv_cac_ratio:.1f}x → {action}")
def export_csv_results(cohort_results: list[UnitEconomicsResult], channel_results: list[UnitEconomicsResult]) -> str:
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["Type", "Label", "Customers", "CAC", "ARPA_Monthly", "Gross_Margin_Pct",
"Monthly_Churn", "LTV", "LTV_CAC_Ratio", "Payback_Months",
"Retention_M6", "Retention_M12"])
for r in cohort_results:
writer.writerow(["cohort", r.label, r.customers, round(r.cac, 2), round(r.arpa, 2),
r.gross_margin_pct, round(r.monthly_churn, 4),
round(r.ltv, 2) if r.ltv != float("inf") else "inf",
round(r.ltv_cac_ratio, 2) if r.ltv_cac_ratio != float("inf") else "inf",
round(r.payback_months, 2) if r.payback_months != float("inf") else "inf",
round(r.retention_m6, 3) if r.retention_m6 else "",
round(r.retention_m12, 3) if r.retention_m12 else ""])
for r in channel_results:
writer.writerow(["channel", r.label, r.customers, round(r.cac, 2), round(r.arpa, 2),
r.gross_margin_pct, round(r.monthly_churn, 4),
round(r.ltv, 2) if r.ltv != float("inf") else "inf",
round(r.ltv_cac_ratio, 2) if r.ltv_cac_ratio != float("inf") else "inf",
round(r.payback_months, 2) if r.payback_months != float("inf") else "inf",
"", ""])
return buf.getvalue()
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
def make_sample_cohorts() -> list[CohortData]:
"""
Series A SaaS company, 8 quarters of cohort data.
Shows a business improving on all dimensions over time.
"""
return [
CohortData(
label="Q1 2023", acquisition_period="Jan-Mar 2023",
customers_acquired=12, total_cac_spend=54_000,
gross_margin_pct=0.68,
monthly_revenue=[
10_200, 9_600, 9_100, 8_700, 8_300, 8_000, # M1-M6
7_800, 7_600, 7_400, 7_200, 7_000, 6_800, # M7-M12
6_700, 6_600, 6_500, 6_400, 6_300, 6_200, # M13-M18
6_100, 6_000, 5_900, 5_800, 5_700, 5_600, # M19-M24
],
),
CohortData(
label="Q2 2023", acquisition_period="Apr-Jun 2023",
customers_acquired=15, total_cac_spend=60_000,
gross_margin_pct=0.69,
monthly_revenue=[
13_500, 12_900, 12_500, 12_100, 11_800, 11_500,
11_300, 11_100, 10_900, 10_700, 10_500, 10_300,
10_200, 10_100, 10_000, 9_900, 9_800, 9_700,
],
),
CohortData(
label="Q3 2023", acquisition_period="Jul-Sep 2023",
customers_acquired=18, total_cac_spend=63_000,
gross_margin_pct=0.70,
monthly_revenue=[
16_200, 15_800, 15_400, 15_100, 14_800, 14_600,
14_400, 14_200, 14_000, 13_900, 13_800, 13_700,
13_600, 13_500, 13_400, 13_300,
],
),
CohortData(
label="Q4 2023", acquisition_period="Oct-Dec 2023",
customers_acquired=22, total_cac_spend=70_400,
gross_margin_pct=0.71,
monthly_revenue=[
20_900, 20_500, 20_200, 19_900, 19_700, 19_500,
19_300, 19_100, 19_000, 18_900, 18_800, 18_700,
],
),
CohortData(
label="Q1 2024", acquisition_period="Jan-Mar 2024",
customers_acquired=28, total_cac_spend=81_200,
gross_margin_pct=0.72,
monthly_revenue=[
27_200, 26_900, 26_600, 26_400, 26_200, 26_000,
25_800, 25_700, 25_600, 25_500,
],
),
CohortData(
label="Q2 2024", acquisition_period="Apr-Jun 2024",
customers_acquired=34, total_cac_spend=91_800,
gross_margin_pct=0.72,
monthly_revenue=[
33_300, 33_000, 32_800, 32_600, 32_400, 32_200,
],
),
CohortData(
label="Q3 2024", acquisition_period="Jul-Sep 2024",
customers_acquired=40, total_cac_spend=100_000,
gross_margin_pct=0.73,
monthly_revenue=[
39_600, 39_400, 39_200,
],
),
CohortData(
label="Q4 2024", acquisition_period="Oct-Dec 2024",
customers_acquired=47, total_cac_spend=112_800,
gross_margin_pct=0.73,
monthly_revenue=[
47_000,
],
),
]
def make_sample_channels() -> list[ChannelData]:
"""
Q4 2024 channel breakdown. Blended looks fine; per-channel reveals problems.
"""
return [
ChannelData("Organic / SEO", spend=9_500, customers_acquired=14, avg_arpa=950, gross_margin_pct=0.73, avg_monthly_churn=0.015),
ChannelData("Paid Search (SEM)", spend=48_000, customers_acquired=18, avg_arpa=980, gross_margin_pct=0.73, avg_monthly_churn=0.020),
ChannelData("Paid Social", spend=32_000, customers_acquired=8, avg_arpa=900, gross_margin_pct=0.72, avg_monthly_churn=0.025),
ChannelData("Content / Inbound", spend=11_000, customers_acquired=6, avg_arpa=1100, gross_margin_pct=0.74, avg_monthly_churn=0.012),
ChannelData("Outbound SDR", spend=22_000, customers_acquired=4, avg_arpa=1200, gross_margin_pct=0.73, avg_monthly_churn=0.022),
ChannelData("Events / Webinars", spend=18_500, customers_acquired=3, avg_arpa=1050, gross_margin_pct=0.72, avg_monthly_churn=0.028),
ChannelData("Partner / Referral", spend=7_800, customers_acquired=7, avg_arpa=1000, gross_margin_pct=0.73, avg_monthly_churn=0.013),
]
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description="Unit Economics Analyzer")
parser.add_argument("--csv", action="store_true", help="Export results as CSV to stdout")
args = parser.parse_args()
cohorts = make_sample_cohorts()
channels = make_sample_channels()
print("\n" + "="*80)
print(" UNIT ECONOMICS ANALYZER")
print(" Sample Company: Series A SaaS | Q4 2024 Snapshot")
print(" Gross Margin: ~72% | Monthly Churn: derived from cohort data")
print("="*80)
cohort_results = [analyze_cohort(c) for c in cohorts]
channel_results = [analyze_channel(c) for c in channels]
print_cohort_analysis(cohort_results)
print_channel_analysis(channel_results, channels)
# Health summary
print("\n" + "="*80)
print(" HEALTH SUMMARY")
print("="*80)
latest = cohort_results[-1]
prev = cohort_results[-4] if len(cohort_results) >= 4 else cohort_results[0]
print(f"\n Latest Cohort ({latest.label}):")
print(f" CAC: {fmt(latest.cac)}")
ltv_str = fmt(latest.ltv) if latest.ltv != float("inf") else "∞"
ltv_cac_str = f"{latest.ltv_cac_ratio:.1f}x" if latest.ltv_cac_ratio != float("inf") else "∞"
payback_str = f"{latest.payback_months:.1f} months" if latest.payback_months != float("inf") else "∞"
print(f" LTV: {ltv_str}")
print(f" LTV:CAC: {ltv_cac_str} (target: > 3x)")
print(f" CAC Payback: {payback_str} (target: < 18mo)")
print(f" Rating: {rating(latest.ltv_cac_ratio, latest.payback_months)}")
# Trend vs 4 quarters ago
print(f"\n Trend vs {prev.label}:")
cac_delta = (latest.cac - prev.cac) / prev.cac * 100
ltv_delta_str = "n/a"
if latest.ltv != float("inf") and prev.ltv != float("inf"):
ltv_delta = (latest.ltv - prev.ltv) / prev.ltv * 100
ltv_delta_str = f"{ltv_delta:+.1f}%"
cac_str = "↓ Better" if cac_delta < 0 else "↑ Worse"
print(f" CAC: {cac_delta:+.1f}% ({cac_str})")
print(f" LTV: {ltv_delta_str}")
print("\n Benchmark Reference:")
print(" LTV:CAC > 5x → Scale aggressively")
print(" LTV:CAC 3-5x → Healthy; grow at current pace")
print(" LTV:CAC 2-3x → Marginal; optimize before scaling")
print(" LTV:CAC < 2x → Acquiring unprofitably; stop and fix")
print(" Payback < 12mo → Outstanding capital efficiency")
print(" Payback 12-18mo → Good for B2B SaaS")
print(" Payback > 24mo → Requires long-dated capital to scale")
if args.csv:
print("\n\n--- CSV EXPORT ---\n")
sys.stdout.write(export_csv_results(cohort_results, channel_results))
if __name__ == "__main__":
main()
Marketing leadership for scaling companies. Brand positioning, growth model design, marketing budget allocation, and marketing org design. Use when designing...
---
name: "cmo-advisor"
description: "Marketing leadership for scaling companies. Brand positioning, growth model design, marketing budget allocation, and marketing org design. Use when designing brand strategy, selecting growth models (PLG vs sales-led vs community-led), allocating marketing budgets, building marketing teams, or when user mentions CMO, brand strategy, growth model, CAC, LTV, channel mix, or marketing ROI."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: cmo-leadership
updated: 2026-03-05
python-tools: marketing_budget_modeler.py, growth_model_simulator.py
frameworks: brand-positioning, growth-frameworks, marketing-org
---
# CMO Advisor
Strategic marketing leadership — brand positioning, growth model design, budget allocation, and org design. Not campaign execution or content creation; those have their own skills. This is the engine.
## Keywords
CMO, chief marketing officer, brand strategy, brand positioning, growth model, product-led growth, PLG, sales-led growth, community-led growth, marketing budget, CAC, customer acquisition cost, LTV, lifetime value, channel mix, marketing ROI, pipeline contribution, marketing org, category design, competitive positioning, growth loops, payback period, MQL, pipeline coverage
## Quick Start
```bash
# Model budget allocation across channels, project MQL output by scenario
python scripts/marketing_budget_modeler.py
# Project MRR growth by model, show impact of channel mix shifts
python scripts/growth_model_simulator.py
```
**Reference docs (load when needed):**
- `references/brand_positioning.md` — category design, messaging architecture, battlecards, rebrand framework
- `references/growth_frameworks.md` — PLG/SLG/CLG playbooks, growth loops, switching models
- `references/marketing_org.md` — team structure by stage, hiring sequence, agency vs. in-house
---
## The Four CMO Questions
Every CMO must own answers to these — no one else in the C-suite can:
1. **Who are we for?** — ICP, positioning, category
2. **Why do they choose us?** — Differentiation, messaging, brand
3. **How do they find us?** — Growth model, channel mix, demand gen
4. **Is it working?** — CAC, LTV:CAC, pipeline contribution, payback period
---
## Core Responsibilities (Brief)
**Brand & Positioning** — Define category, build messaging architecture, maintain competitive differentiation. Details → `references/brand_positioning.md`
**Growth Model** — Choose and operate the right acquisition engine: PLG, sales-led, community-led, or hybrid. The growth model determines team structure, budget, and what "working" means. Details → `references/growth_frameworks.md`
**Marketing Budget** — Allocate from revenue target backward: new customers needed → conversion rates by stage → MQLs needed → spend by channel based on CAC. Run `marketing_budget_modeler.py` for scenarios.
**Marketing Org** — Structure follows growth model. Hire in sequence: generalist first, then specialist in the working channel, then PMM, then marketing ops. Details → `references/marketing_org.md`
**Channel Mix** — Audit quarterly: MQLs, cost, CAC, payback, trend. Scale what's improving. Cut what's worsening. Don't optimize a channel that isn't in the strategy.
**Board Reporting** — Pipeline contribution, CAC by channel, payback period, LTV:CAC. Not impressions. Not MQLs in isolation.
---
## Key Diagnostic Questions
Ask these before making any strategic recommendation:
- What's your CAC **by channel** (not blended)?
- What's the payback period on your largest channel?
- What's your LTV:CAC ratio?
- What % of pipeline is marketing-sourced vs. sales-sourced?
- Where do your **best customers** (highest LTV, lowest churn) come from?
- What's your MQL → Opportunity conversion rate? (proxy for lead quality)
- Is this brand work or performance marketing? (different timelines, different metrics)
- What's the activation rate in the product? (PLG signal)
- If a prospect doesn't buy, why not? (win/loss data)
---
## CMO Metrics Dashboard
| Category | Metric | Healthy Target |
|----------|--------|---------------|
| **Pipeline** | Marketing-sourced pipeline % | 50–70% of total |
| **Pipeline** | Pipeline coverage ratio | 3–4x quarterly quota |
| **Pipeline** | MQL → Opportunity rate | > 15% |
| **Efficiency** | Blended CAC payback | < 18 months |
| **Efficiency** | LTV:CAC ratio | > 3:1 |
| **Efficiency** | Marketing % of total S&M spend | 30–50% |
| **Growth** | Brand search volume trend | ↑ QoQ |
| **Growth** | Win rate vs. primary competitor | > 50% |
| **Retention** | NPS (marketing-sourced cohort) | > 40 |
---
## Red Flags
- No defined ICP — "companies with 50-1000 employees" is not an ICP
- Marketing and sales disagree on what an MQL is (this is always a system problem, not a people problem)
- CAC tracked only as a blended number — channel-level CAC is non-negotiable
- Pipeline attribution is self-reported by sales reps, not CRM-timestamped
- CMO can't answer "what's our payback period?" without a 48-hour research project
- Brand work and performance marketing have no shared narrative — they're contradicting each other
- Marketing team is producing content with no documented positioning to anchor it
- Growth model was chosen because a competitor uses it, not because the product/ACV/ICP fits
---
## Integration with Other C-Suite Roles
| When... | CMO works with... | To... |
|---------|-------------------|-------|
| Pricing changes | CFO + CEO | Understand margin impact on positioning and messaging |
| Product launch | CPO + CTO | Define launch tier, GTM motion, messaging |
| Pipeline miss | CFO + CRO | Diagnose: volume problem, quality problem, or velocity problem |
| Category design | CEO | Secure multi-year organizational commitment to the narrative |
| New market entry | CEO + CFO | Validate ICP, budget, localization requirements |
| Sales misalignment | CRO | Align on MQL definition, SLA, and pipeline ownership |
| Hiring plan | CHRO | Define marketing headcount and skill profile by stage |
| Retention insights | CCO | Use expansion and churn data to sharpen ICP and messaging |
| Competitive threat | CEO + CRO | Coordinate battlecards, win/loss, repositioning response |
---
## Resources
- **References:** `references/brand_positioning.md`, `references/growth_frameworks.md`, `references/marketing_org.md`
- **Scripts:** `scripts/marketing_budget_modeler.py`, `scripts/growth_model_simulator.py`
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- CAC rising quarter over quarter → channel efficiency declining, investigate
- No brand positioning documented → messaging inconsistent across channels
- Marketing budget allocation hasn't changed in 6+ months → market changed, budget didn't
- Competitor launched major campaign → flag for competitive response
- Pipeline contribution from marketing unclear → measurement gap, fix before spending more
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Plan our marketing budget" | Channel allocation model with CAC targets per channel |
| "Position us vs competitors" | Positioning map + messaging framework + proof points |
| "Design our growth model" | Growth projection with channel mix scenarios |
| "Build the marketing team" | Hiring plan with sequence, roles, agency vs in-house |
| "Marketing board section" | Pipeline contribution report with channel ROI |
## Reasoning Technique: Recursion of Thought
Draft a marketing strategy, then critique it from the customer's perspective. Refine based on the critique. Repeat until the strategy survives scrutiny.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:references/brand_positioning.md
# Brand Positioning Reference
Practical frameworks for defining, communicating, and defending your market position. Not theory — applied tools for CMOs who need to get this right.
---
## 1. Category Design Frameworks
### The Category Design Principle
Every product exists in a category — either one you define or one someone else defined. If you're not designing your category, your competitors are designing it for you, and they'll design it to exclude you.
**Category design is not renaming an existing category.** It's declaring that the existing category no longer solves the problem adequately, and that a new category — which you happen to lead — is required.
### The Three-Act Category Design Narrative
**Act 1: Name the problem**
Identify a problem that's real, growing, and underserved. Not a problem you invented — a problem your best customers articulate before they've heard your pitch.
> "Enterprise software teams are deploying faster than ever, but their security reviews still take 3 weeks — because security was built for a world where deployments happen monthly, not hourly."
**Act 2: Define the new category**
Name the category in terms of the outcome, not the feature. The category name should describe what customers achieve, not what the product does.
> "Continuous security" — not "automated security scanning" or "DevSecOps platform."
**Act 3: Position yourself as the category leader**
You can't just claim leadership — you need proof: customers, analysts, community, content, events. Leadership is built, not declared.
> "Snyk is building the continuous security category. 1.2M developers have adopted Snyk. Gartner lists us as a Cool Vendor in AppSec."
### When Category Design Works
| Condition | Explanation |
|-----------|-------------|
| Market timing | The problem is growing but the existing category is inadequate |
| CEO commitment | Category design is a 3-5 year initiative, not a marketing campaign |
| Analyst alignment | Gartner, Forrester, or G2 need to recognize your category |
| Community | Practitioners adopt the vocabulary before buyers do |
| Content moat | You publish the defining content for the category before competitors |
### Category Design Pitfalls
- **Naming the category after yourself:** "The [Your Company] Category" is not a category. It's a vanity.
- **Categories that don't solve analyst definitions:** If Gartner doesn't have a Magic Quadrant for your category, you're fighting uphill.
- **Jargon without adoption:** If your category name requires a two-paragraph explanation, it won't stick.
- **Starting a category war you can't win:** If an incumbent can copy your category name and launch in 90 days, you don't have a defensible category.
### The Lightning Strike Strategy
Category design requires concentrated, coordinated effort — not slow drip. Execute these simultaneously:
1. **Major piece of research or data** (the "State of X" report)
2. **Category-defining event** (host it, don't just attend)
3. **Analyst briefing** (educate Gartner/Forrester on the category before they define it themselves)
4. **Book or manifesto** (long-form content that becomes the category Bible)
5. **Community formation** (a Slack group, a conference, a certification that practitioners want)
Do all five within a 3-month window. This creates gravity around your category claim.
---
## 2. Messaging Architecture
### The Messaging Hierarchy
Every piece of content — from a tweet to a 60-page whitepaper — should trace back to this hierarchy. When it doesn't, you have messaging drift.
```
Level 1: Brand Promise
"[Company] [verb] [outcome] for [audience]"
→ Doesn't change. This is the north star.
Level 2: Positioning Statement (internal)
For [target customer] who [has this problem],
[Company] is the [market category] that [differentiated capability]
unlike [alternatives], [Company] [proof of differentiation].
Level 3: Value Propositions (3-4 max, one per key outcome)
Each VP: headline (5-8 words) + 2-3 sentence explanation + proof point
Level 4: Proof Points
Data, case studies, certifications, analyst recognition — evidence for each VP
Level 5: Channel Adaptations
Website copy, sales deck, ad copy, email — same hierarchy, different format
```
### Writing a Positioning Statement
The Geoffrey Moore / April Dunford format is still the best framework:
**Template:**
```
For [specific target customer]
who [has this specific, painful problem],
[Company name] is the [market category]
that [key differentiated capability].
Unlike [primary alternatives],
[Company] [proof of differentiation — something measurable or unique].
```
**Bad example (too generic):**
> For B2B companies who want to grow faster, Acme is the marketing platform that helps you get more leads. Unlike other platforms, Acme is easy to use and powerful.
**Good example (specific and falsifiable):**
> For DevOps teams in regulated industries who spend 20% of their sprint cycles on compliance reviews, Acme is the compliance automation platform that embeds regulatory checks directly into the CI/CD pipeline. Unlike manual compliance tools that create a separate review queue, Acme's policy-as-code approach reduces compliance-related cycle time by 60% without slowing deployments.
**Test your positioning statement:**
1. Can a competitor say the exact same thing? (If yes, it's not differentiated)
2. Does it describe what you do or what the customer gets? (Should be the latter)
3. Would your best customer say "yes, that's exactly my problem"? (If not, wrong ICP)
4. Is it falsifiable? (Claims you can't prove are liabilities)
### Value Proposition Development
**Structure for each VP:**
| Element | Description | Example |
|---------|-------------|---------|
| Outcome headline | What changes for the customer (5-8 words) | "Ship features 3x faster" |
| The problem | Why this matters now (1 sentence) | "Compliance reviews block 40% of releases in regulated industries" |
| Our approach | How we solve it differently (1-2 sentences) | "Policy-as-code embeds checks in the pipeline instead of adding a gate at the end" |
| Proof | Evidence this is real (1 sentence + data point) | "Customers reduce compliance cycle time by 60% in the first 90 days" |
**3-VP Architecture is the standard:**
- VP1: Core outcome (what most customers primarily buy for)
- VP2: Secondary benefit (makes the decision easier or stickier)
- VP3: Differentiator (what tips competitive decisions in your favor)
### Proof Point Hierarchy
Not all proof is equal. When you make a claim, match the strength of your proof to the importance of the claim.
| Proof Type | Strength | Best Used For |
|------------|---------|--------------|
| Third-party data (analyst report, research) | Highest | Category claims, market size |
| Customer ROI data with name | High | Value propositions |
| Customer quote with name and company | Medium-high | Specific pain points and outcomes |
| Aggregated customer data ("customers report…") | Medium | Directional claims |
| Internal testing or benchmark | Medium-low | Product capability claims |
| "Designed to…" or "built for…" | Low | Product direction only |
| "We believe…" or "we think…" | Lowest | Vision statements only |
**Proof point development process:**
1. Write the claim you want to make
2. Identify the strongest available proof
3. If proof is weak, either soften the claim or invest in getting better proof
4. Never publish a claim without knowing what happens when a skeptic asks "prove it"
---
## 3. Competitive Positioning Maps
### The Two-Axis Map
Choose two dimensions that:
1. Both matter to your target buyer
2. Create clear differentiation between you and competitors
3. You can credibly defend
**Choosing the axes:**
- Axis 1 should show a dimension where you win and most competitors cluster on the wrong side
- Axis 2 should show a dimension buyers care about deeply (ease, speed, breadth, price, compliance, etc.)
**What to avoid:**
- "Quality" vs. "Price" — too generic, every company claims the top-left
- Dimensions your competitors can match in one release cycle
- Dimensions that only your product team understands, not buyers
### Competitive Analysis Template
For each major competitor:
**Company:** _______________
| Dimension | What They Claim | What Customers Actually Experience | Gap |
|-----------|----------------|-----------------------------------|-----|
| Positioning | | | |
| Primary differentiator | | | |
| Pricing | | | |
| Ideal customer | | | |
| Weakness (win/loss data) | | | |
| What they say about you | | | |
**Sources for competitive intelligence:**
- Win/loss interviews (primary source — nothing beats this)
- G2/Capterra reviews (what customers say publicly)
- Glassdoor (tells you about internal culture and focus)
- LinkedIn job postings (what they're building next)
- Their pricing page changes (what they're competing on)
- Conference talks from their product and sales leaders
### Battlecard Format
One page per competitor. Used by sales, not marketing.
```
COMPETING AGAINST: [Competitor Name]
WHY CUSTOMERS CONSIDER THEM:
(2-3 bullets — be honest about their appeal)
OUR DIFFERENTIATION:
(2-3 bullets — factual, not marketing language)
THE LANDMINE QUESTION:
(One question that exposes their weakness. The answer should make the buyer uncomfortable choosing them.)
Example: "How long does your typical implementation take? And what's your SLA if it runs over?"
OUR PROOF POINTS IN THIS COMPARISON:
- [Customer name] switched from [competitor] after [specific reason], saw [specific result]
- [Data point that directly contradicts competitor's primary claim]
THEIR LIKELY COUNTER-MOVES:
(What will they say about us? How do we respond?)
WHEN TO WALK AWAY:
(If the prospect values X more than Y, we are not the right fit — say so)
```
---
## 4. Brand Voice Development
### What Brand Voice Is (and Isn't)
**Brand voice is NOT:**
- A list of adjectives ("we are professional, innovative, and customer-focused")
- The tone you use in formal communications
- The font and color palette (that's visual identity)
**Brand voice IS:**
- How the company sounds across every written touchpoint
- Consistent enough to be recognizable, flexible enough to be human
- Grounded in what your best customers actually value
### The Voice Attribute Framework
Define 3-4 voice attributes. For each:
1. **What it means** (in one sentence)
2. **What it sounds like** (one example)
3. **What it doesn't mean** (the common mistake that goes wrong)
**Example:**
| Attribute | Means | Sounds like | Doesn't mean |
|-----------|-------|------------|--------------|
| Direct | We say what we mean without hedging | "Your compliance review takes 3 weeks. It shouldn't." | Blunt, rude, or dismissive |
| Expert | We speak from depth, not from trend | "Here's why most security gates fail at scale, and what actually works." | Jargon-heavy or condescending |
| Honest | We acknowledge what we don't do | "We're not the best fit if you need a one-size-fits-all platform." | Self-deprecating or uncertain |
| Human | Real people write for real people | "Deploying on a Friday? Here's what we'd check first." | Casual, unprofessional |
### Voice Consistency Testing
Take a random sample of 10 recent pieces of content:
- Website homepage and pricing page
- 3 blog posts from different authors
- 5 outbound emails from sales
- 3 social posts
- 1 press release
Score each on: Does this sound like us? (1-5)
Average < 3: You have a brand voice problem. The cause is usually no documented guidelines, or guidelines that exist but aren't enforced.
### Voice in Different Contexts
The attribute stays the same. The tone adjusts.
| Context | Tone adjustment | Example of "Direct" |
|---------|----------------|---------------------|
| Homepage | Confident | "Compliance reviews don't have to slow you down." |
| Technical docs | Precise | "Set the policy threshold to 0.95 to enforce mandatory approval." |
| Error messages | Helpful | "That didn't work. Here's the most common reason why, and how to fix it." |
| Support | Empathetic | "That's frustrating. Here's what happened and what we're doing about it." |
| Sales outreach | Respectful | "Most teams in your space have this problem. Worth 20 minutes to explore?" |
---
## 5. Rebrand Decision Framework
### When Rebrands Succeed vs. Fail
**Successful rebrands:**
- Driven by a genuine strategic shift (new category, new ICP, new market)
- Have internal alignment before external launch
- Are accompanied by product and messaging changes — not just visual
- Have a 6-12 month transition plan for existing customers
**Failed rebrands:**
- Driven by internal boredom with the old brand
- Executed as a "refresh" without repositioning the value proposition
- Lack leadership conviction (executives still describe the company in the old terms)
- Launch with a new logo but same product, same messaging, same ICP
### The Rebrand Decision Matrix
Answer each question. More "yes" answers = more likely rebrand is warranted.
| Question | Yes | No |
|----------|-----|-----|
| Has our ICP changed significantly in the last 18 months? | Rebrand | Stay |
| Are we entering a new market where the current brand creates friction? | Rebrand | Stay |
| Does the brand name have negative associations in the market? | Rebrand | Stay |
| Has an acquisition changed our core identity? | Rebrand | Stay |
| Is the current brand actively hurting sales conversations? (evidence required) | Rebrand | Stay |
| Are we bored with the brand? | Stay | — |
| Did leadership change? | Stay | — |
| Are competitors rebranding? | Stay | — |
Score: 3+ "Rebrand" answers with evidence = worth a serious evaluation.
### Rebrand Risk Assessment
**Name change** is the highest-risk rebrand element. Before committing:
- Legal: trademark availability in all target markets
- SEO: 18-24 months to recover domain authority after a domain change
- Customer: existing customers need to update all integrations, contracts, documentation
- Analyst: re-education of Gartner, Forrester, G2 category definitions
- Employee: company identity shift is a culture event, not just an HR task
**Minimum viable rebrand (lower risk):**
1. New positioning and messaging (always worth doing if positioning is wrong)
2. Visual identity refresh (keep the name, update the look)
3. Tagline change (the cheapest, lowest-risk brand change)
**Full rebrand (high risk, sometimes necessary):**
1. New company name and domain
2. New visual identity
3. New positioning and messaging
4. New category narrative
### Rebrand Execution Checklist
**Pre-launch (90 days):**
- [ ] Finalize positioning before finalizing design (in that order)
- [ ] Legal trademark clearance in all target markets
- [ ] Domain secured (with redirects planned)
- [ ] Internal alignment: every leader can describe the new positioning in one sentence
- [ ] Customer comms plan (existing customers, especially enterprise, need advance notice)
- [ ] Analyst briefings scheduled (Gartner, Forrester — brief them before launch)
- [ ] PR plan finalized
**Launch (day 1):**
- [ ] Website flipped
- [ ] Social profiles updated
- [ ] Email signatures updated company-wide
- [ ] Sales deck updated
- [ ] Press release published
- [ ] Existing customers notified (email from CEO or CMO, not marketing automation)
**Post-launch (90 days):**
- [ ] SEO monitoring (watch for ranking drops on key terms)
- [ ] Win rate monitoring (did conversion change?)
- [ ] Employee feedback (are they using the new messaging correctly?)
- [ ] Partner/channel update (resellers, integrations, directories)
- [ ] Analyst follow-up (did they update their reports?)
---
## Quick Reference: Brand Positioning Diagnostic
Use this as an audit against your current positioning:
| Check | Pass | Fail |
|-------|------|------|
| Can every sales rep state the positioning in one sentence without looking it up? | ✓ | Positioning isn't working |
| Is the ICP specific enough to disqualify companies? | ✓ | ICP is too broad |
| Does the homepage lead with customer outcome, not product features? | ✓ | Copy needs rewrite |
| Can you name 3 companies you're NOT a good fit for? | ✓ | Positioning is unfocused |
| Do win/loss interviews confirm the stated differentiator? | ✓ | Differentiator is assumed, not proven |
| Is the category name used by analysts or industry media? | ✓ | Category design needed |
| Does every piece of content trace back to a VP from the hierarchy? | ✓ | Messaging drift — need guidelines |
FILE:references/growth_frameworks.md
# Growth Frameworks Reference
Playbooks for PLG, sales-led, community-led, and hybrid growth models. Includes growth loops, funnel design, and guidance on when and how to switch models.
---
## 1. Product-Led Growth (PLG) Playbook
### What PLG Actually Is
PLG means the product is the primary distribution mechanism. Not "we have a free trial." Not "our product is self-serve." PLG means the product creates acquisition, retention, and expansion — and does so at a scale and cost no sales team can match.
**The minimum requirements for PLG to work:**
1. **Fast time-to-value:** Users must get a meaningful outcome within one session (ideally < 30 minutes)
2. **Low friction to start:** No sales call, no implementation project, no credit card required (for top of funnel)
3. **Built-in virality or network effects:** Usage creates exposure or value that draws in other users
4. **Self-serve monetization or expansion path:** Freemium → paid, or individual → team → company
If any of these is missing, you don't have PLG — you have a website with a free trial.
### PLG Funnel: The Four Stages
**Stage 1: Acquisition**
The user discovers and signs up for the product without talking to sales.
Key channels:
- Organic search (SEO targeting jobs-to-be-done searches)
- Product hunt launches
- Referral and invite loops (users share the product with colleagues)
- Developer communities and open-source contributions
Metric: Visitor-to-signup rate
Benchmark: 2-8% for B2B SaaS (varies heavily by product complexity)
**Stage 2: Activation**
The user reaches the "aha moment" — the point where the product delivers its core value for the first time.
Finding the aha moment:
- Look at the behaviors that differentiate users who stay from users who churn in the first 30 days
- The aha moment is not creating an account. It's completing the first outcome.
- For Slack: sending a message in a real channel
- For Dropbox: adding a file from a second device
- For HubSpot: publishing a form that captures a real lead
Metric: Activation rate (% of signups who complete the aha moment action within 7 days)
Benchmark: 25-40% is strong. < 15% means the onboarding is broken.
**Stage 3: Retention**
Users return to the product and build habitual use.
Retention analysis:
- Cohort retention curves (by signup week/month)
- Day 1, Day 7, Day 30, Day 90 retention rates
- Feature adoption by retained vs. churned users (which features predict retention?)
Metric: D30 retention rate (% of users still active 30 days after signup)
Benchmark: > 40% D30 retention is strong for B2B products
**Stage 4: Revenue**
Self-serve conversion from free to paid, or expansion from individual to team.
PQL (Product-Qualified Lead) signals:
- Reached a usage limit (invites, storage, seats)
- Used a premium feature in trial mode
- Team size on the account reached a threshold
- High-frequency usage above a defined threshold
Metric: PQL conversion rate (% of PQLs who convert to paid within 30 days)
Benchmark: 15-30% for well-designed PLG products
### PLG Expansion Model
PLG growth compounds through account expansion:
```
Individual user discovers product
→ Gets value, invites teammates
→ Team adopts product
→ Becomes department-wide
→ Finance/IT gets involved
→ Enterprise contract
```
This is "bottom-up" enterprise: individual adoption precedes company-wide purchase. It's also the most defensible moat — when every engineer in the company uses your product individually, procurement cancellation is very hard.
**Expansion levers:**
- Seat-based pricing (more users = more revenue, aligned with value)
- Usage-based pricing (more usage = more value = more revenue)
- Feature gating (team/enterprise features visible but gated, creating pull to upgrade)
- Admin discovery (usage reports surface to managers who didn't know they had a product champion)
### PLG Diagnostic
| Question | Healthy | Unhealthy |
|----------|---------|-----------|
| Time-to-value | < 30 minutes | > 2 hours |
| Activation rate | > 30% | < 15% |
| D30 retention | > 40% | < 20% |
| PQL conversion | > 15% | < 5% |
| NPS from self-serve users | > 40 | < 20 |
| Viral coefficient | > 0.3 | < 0.1 |
### PLG Team Structure
```
Head of Growth (often VP Product or VP Marketing)
├── Growth PM (owns activation and retention loops in product)
├── Growth Engineer (2-3 engineers dedicated to growth experiments)
├── Data Analyst (experimentation, funnel analysis, cohort reports)
└── Growth Marketer (acquisition, SEO, referral programs)
```
The growth team sits between product and marketing. This is intentional — they own the product loops that drive acquisition and retention.
---
## 2. Sales-Led Growth (SLG) Model
### The SLG System
In SLG, marketing's job is to fill the sales pipeline. Sales converts it. The system only works if marketing and sales agree on definitions, SLAs, and shared metrics.
**The SLG funnel:**
```
Awareness (Impressions, reach, brand search)
↓
Lead (Name + contact info captured)
↓
MQL — Marketing Qualified Lead (meets ICP criteria, intent signal detected)
↓ [Marketing → Sales handoff]
SAL — Sales Accepted Lead (sales reviews and accepts the lead)
↓
SQL — Sales Qualified Lead (sales confirms budget, authority, need, timeline)
↓
Opportunity (Formal deal in pipeline, has a close date)
↓
Closed-Won
```
**The MQL definition problem:**
Most marketing-sales friction traces to an unclear MQL definition. The MQL should be:
- ICP-matched (company size, industry, role)
- Intent-signaled (visited pricing page, attended webinar, downloaded high-intent content)
- Not just email address + "subscribed to newsletter"
**A concrete MQL definition:**
> Company 50-500 employees, B2B SaaS, role is VP Engineering or CTO or CISO, AND has performed 2+ of: attended webinar, visited pricing page, requested demo, downloaded security report, attended event.
This definition makes the MQL useful. If you can't score it in your CRM without human judgment, it's not a definition — it's a guideline.
### SLG Conversion Rate Benchmarks
| Stage | Average B2B SaaS | Top Quartile |
|-------|-----------------|--------------|
| Lead → MQL | 5-15% | > 20% |
| MQL → SAL | 50-70% | > 75% |
| SAL → SQL | 30-50% | > 60% |
| SQL → Opportunity | 60-80% | > 85% |
| Opportunity → Closed-Won | 20-30% | > 40% |
**End-to-end:** Lead → Closed-Won: 1-5% (wide range by ACV and ICP quality)
### Pipeline Coverage Mechanics
A healthy SLG pipeline has 3-4x coverage against quota.
If a sales rep has a $500K quarterly quota:
- They need $1.5M-$2M in active pipeline
- Pipeline must be distributed across stages (not all "prospecting")
- Stage distribution benchmark: 30% early, 40% mid, 30% late
Insufficient coverage (< 3x) is a lagging indicator of a miss — by the time coverage is low, it's too late to recover in the same quarter. Coverage should be tracked weekly.
### SLG Demand Generation Channels
**High-intent channels (bottom of funnel):**
- Paid search on buying-intent keywords (e.g., "[competitor] alternative", "best [category] software")
- Review site presence (G2, Capterra) — buyers use these before vendor websites
- Outbound SDR targeting specific accounts (ABM)
**Medium-intent channels (middle of funnel):**
- Webinars and virtual events (capture active learners)
- Gated content (guides, benchmarks, templates — ICP-specific)
- Retargeting to website visitors
**Awareness channels (top of funnel):**
- Content and SEO (captures people learning about the problem)
- Podcast sponsorships, industry media
- Conference sponsorship and speaking
- Paid social (LinkedIn for B2B)
### ABM (Account-Based Marketing) in SLG
ABM flips the funnel: instead of generating leads and filtering for good ones, you start with target accounts and run coordinated campaigns against them.
**Tiers:**
- **Tier 1 (1:1):** 5-20 strategic accounts, fully customized campaigns, dedicated SDR+AE pairs, executive outreach
- **Tier 2 (1:few):** 50-200 accounts, programmatic personalization, SDR sequences, targeted events
- **Tier 3 (1:many):** 500+ accounts, standard campaigns with light personalization
ABM requires tight sales/marketing alignment. If sales doesn't work the accounts marketing targets, ABM produces zero results.
---
## 3. Community-Led Growth (CLG)
### The CLG Thesis
Community-led growth works when:
1. Your buyers want to learn from peers, not vendors
2. There's a strong practitioner identity (developers, data teams, security, FinOps)
3. Your category is complex enough that buyers need education before purchasing
4. You can commit to building genuine community, not a marketing channel in disguise
**The fundamental rule of CLG:** The community must deliver value to members whether or not they ever buy your product. If the only purpose of the community is to sell to members, the community will die.
### CLG Stages
**Stage 1: Find the community**
The community often exists before you build it. Find where your practitioners already gather:
- Slack groups, Discord servers
- Subreddits and LinkedIn groups
- Conference hallways
- Open-source repositories
Before building, participate. Earn trust. Understand the conversations.
**Stage 2: Become the knowledge hub**
Establish your company as the best source of information on the category problem:
- Publish the benchmark study everyone references
- Host the conference that defines the industry
- Create the certification practitioners want on their resume
- Open-source the tools the community needs
**Stage 3: Build the platform**
Create a dedicated community space (Slack, Discord, forum):
- Community must be practitioner-first, not vendor-first
- Community managers who genuinely care about member value
- Content from members, not just from your company
- Events that build member relationships, not just product demos
**Stage 4: Convert community to customers**
Community members who become customers do so because they trust you, not because you sold them. Conversion paths:
- Community members see peer success with your product
- Product-qualified signals from community members who trial the product
- Direct outreach from sales to active community members (with permission and context)
- Enterprise deals from companies whose employees are active in the community
### CLG Metrics
| Metric | Definition | Health Signal |
|--------|-----------|--------------|
| Monthly active members | Members who post, comment, or engage | > 15% of total members |
| Community-sourced pipeline | $ pipeline where community was first touch | Track and trend |
| Community-influenced pipeline | $ pipeline with any community touchpoint | > 30% of total pipeline |
| NPS of community members vs. non-members | Loyalty difference | Community members should score 20+ pts higher |
| Member-generated content % | % of content posted by non-employees | > 60% is healthy community |
| Time from community join to product trial | | Shortens as community matures |
### CLG Anti-Patterns
- **Community as a newsletter:** If members can't interact with each other, it's not a community — it's a list.
- **Product launches in the community:** Nothing kills community trust faster than using it for sales announcements.
- **Community without a community manager:** Communities left to run themselves become ghost towns or become toxic.
- **Measuring community by member count:** Ghost members are noise. Active engagement is signal.
---
## 4. Hybrid Growth Models
### PLG + SLG ("Product-Led Sales" or PLS)
The most common hybrid at growth stage. PLG handles SMB self-serve; sales closes enterprise.
**The PQL-to-sales handoff:**
Define the triggers that move a product-qualified lead to a sales-assisted motion:
- Company has > X users (e.g., 10+ users on a team account)
- Usage exceeds Y threshold in 30 days
- Account is a named target in the ABM list
- User explicitly requested a demo or upgrade assistance
**The risk:** Sales team ignores PLG pipeline because deal size is smaller. Fix: separate quotas and commission structures for self-serve expansion vs. new enterprise logos.
**The opportunity:** PLG creates pre-qualified champions inside accounts. Sales doesn't have to create interest — they convert it. Win rates in PLS motions are typically 30-50% higher than cold outbound.
### SLG + CLG
Community builds brand and generates inbound pipeline for sales.
This hybrid works when:
- Sales cycles are long (6-18 months)
- Buyers do extensive research before engaging with vendors
- The community validates your credibility before sales conversations begin
**The integration:**
- Community team feeds content insights to demand gen
- Event attendees become high-priority SDR sequences
- Active community members get dedicated AE outreach with community context
- Win/loss analysis includes community touchpoints
### PLG + CLG
The developer/open-source hybrid. PLG handles product adoption; community handles advocacy and content.
**Examples:** HashiCorp (Terraform community + enterprise sales), Elastic (open-source + community + commercial), Tailscale (developer community + self-serve + enterprise).
**How it compounds:**
```
Community member learns from community content
→ Discovers open-source or free tier
→ Gets value in first session
→ Shares experience in community
→ New members discover product through community content
```
---
## 5. Growth Loops vs. Funnels
### The Difference
**A funnel** is linear. It requires constant input at the top to produce output at the bottom. If you stop feeding it, it stops producing.
**A growth loop** is cyclical. Output from one stage becomes input to the next. The system compounds.
### Common Growth Loops
**Viral loop:**
```
User gets value → Invites colleague → Colleague signs up →
Colleague invites another colleague → ...
```
Viral coefficient (K) = (Average invites per user) × (Conversion rate of invites)
- K > 1: Exponential growth (rare)
- K 0.5-1: Strong viral assist
- K < 0.3: Viral is not a meaningful growth driver
**Content SEO loop:**
```
Publish content on [topic] → Ranks in search →
Drives signups → Users share content → Builds backlinks →
Better rankings → More content is possible
```
This loop takes 12-24 months to activate but is extraordinarily defensible once running.
**UGC (User-Generated Content) loop:**
```
Users share their work publicly (templates, analyses, portfolios) →
Others discover the work → They find the product →
They create and share their own work → ...
```
Figma, Notion, Airtable, Canva — all run this loop.
**Data network effect loop:**
```
More users → More data → Better product →
More users attracted → ...
```
LinkedIn, Waze, Duolingo — accuracy or relevance improves as the user base grows.
**Integration loop:**
```
Product integrates with X → X's users discover your product →
More integrations possible → More discovery surfaces → ...
```
Zapier, Slack apps, Salesforce AppExchange — being in the ecosystem creates distribution.
### Building a Growth Loop
**Step 1: Map the current funnel**
Where do customers come from? What are the conversion steps?
**Step 2: Find the output**
What does a successful customer produce?
- Invite emails
- Shared content
- Public work visible to others
- Reviews or testimonials
**Step 3: Design the loop**
How does that output become tomorrow's input to acquisition?
- If they share → is there a landing page that captures the new visitor?
- If they invite → is the invite experience friction-free?
- If they create content → does it rank in search or appear in relevant communities?
**Step 4: Measure loop velocity**
For each loop, measure:
- Cycle time: How long does one full cycle take?
- Conversion at each step: Where does the loop break down?
- Loop coefficient: How many new users does one existing user generate?
---
## 6. When to Switch Growth Models
### The Warning Signs
**PLG-to-SLG triggers:**
- Enterprise accounts are signing up via PLG but aren't expanding without human intervention
- Average deal sizes in enterprise are 10-20x SMB, and you're leaving revenue on the table
- Product adoption in enterprise requires configuration or integration that needs support
- PLG accounts churn at higher rates than sales-assisted accounts
**SLG-to-PLG/PLS triggers:**
- CAC is increasing year-over-year as competition for sales talent intensifies
- Smaller competitors are winning deals with self-serve
- Customers are asking "can I just try this myself?"
- ACV is declining as the market matures and products commoditize
- Sales team efficiency (revenue per sales rep) is declining
**Adding CLG to existing motion:**
- Sales cycles are long and trust is the primary barrier
- SEO and content are generating traffic but low conversion (awareness without trust)
- Competitors are building community and you're not present
- Customer success teams report that customers who participate in user groups retain better
### The Transition Playbook
**Phase 1: Prove it before scaling (months 1-6)**
Don't restructure the team to support the new model before proving it works.
- Run a pilot: 3-5 SDRs testing PLG signals as outreach triggers (for PLG → PLS)
- Or: Launch a beta community with 100 core customers (for adding CLG)
- Measure the metrics of the new model, compare to current model
**Phase 2: Parallel running (months 6-12)**
Run both models simultaneously. Don't kill the current model while building the new one.
- Set clear boundaries on which accounts go to which motion
- Build dedicated teams for each model (don't ask the same people to do both)
- Define success metrics for the new model independently
**Phase 3: Rebalance (months 12-18)**
Once the new model proves its unit economics:
- Shift headcount and budget to the more efficient model
- Keep the old model for the segments where it still works
- Document what the new model requires to sustain itself
**The anti-pattern:** Announcing a model shift without proof, restructuring the team, and discovering after 12 months that the new model doesn't work. By then, the old model's momentum is gone and you've burned a year.
### Growth Model Maturity Matrix
| Dimension | PLG | SLG | CLG |
|-----------|-----|-----|-----|
| Time to first results | 3-6 months | 1-3 months | 12-18 months |
| Requires up-front product investment | High | Low | Medium |
| Scales without linear headcount | Yes | No | Yes |
| Predictable pipeline | Low (early) | High | Low (early) |
| CAC trend over time | Decreases | Flat/increases | Decreases |
| Works for ACV > $50K | Only with SLG assist | Yes | Yes |
| Works for ACV < $5K | Yes | No | Only with PLG |
| Defensibility once established | High | Low | Very high |
FILE:references/marketing_org.md
# Marketing Org Reference
Team structure, hiring sequence, agency decisions, marketing ops, and cross-functional alignment — by company stage.
---
## 1. Marketing Team Structure by Stage
### Pre-Seed / Seed (< $1M ARR, 1–10 people)
Don't hire a marketing team yet. The founders are the marketing team.
What to do instead:
- Founders write content, do sales calls, go to events
- The goal is learning the ICP and finding the channel that works, not scaling anything
- One contractor or agency for specific output (design, SEO audit) is fine
First marketing hire trigger: You have a repeatable sales motion and need to scale it.
---
### Series A ($1M–$5M ARR, 10–30 people)
**Org:**
```
Founding Marketer (Head of Marketing or VP Marketing)
```
One person. Generalist. Capable of writing, running ads, setting up HubSpot, producing a report. Their job is to find what works.
**What they own:**
- Content and SEO foundation
- Paid channel experiments
- Sales enablement basics (1-pager, deck, email sequences)
- Event presence (1-2 conferences)
- Marketing attribution setup (get this right early)
**What they don't own yet:**
- Brand redesign
- Analyst relations
- Partner marketing
- Field marketing team
**CMO vs. VP Marketing at this stage:** VP Marketing. An experienced operator who can build and execute. A CMO's strategic value isn't fully leveraged until there's a team to lead and a budget to allocate.
---
### Series B ($5M–$20M ARR, 30–80 people)
**PLG-first org:**
```
VP Marketing
├── Growth Marketing (acquisition loops, activation, PLG analytics)
├── Product Marketing (positioning, launch, sales enablement)
└── Content & SEO (organic engine)
```
**SLG-first org:**
```
VP Marketing
├── Demand Generation (pipeline creation, paid, digital)
├── Product Marketing (positioning, competitive intel, enablement)
├── Field Marketing (events, regional, ABM)
└── Marketing Operations (CRM, attribution, reporting)
```
**Community-led org:**
```
VP Marketing
├── Community & Developer Relations
├── Content & SEO
└── Product Marketing
```
**At this stage:** Marketing ops becomes critical. Without it, attribution is guesswork and the sales team blames marketing for bad leads.
---
### Series C ($20M–$75M ARR, 80–200 people)
```
CMO
├── Demand Generation
│ ├── Paid Media
│ ├── SEO & Content
│ └── Marketing Operations
├── Product Marketing
│ ├── Core PMMs (by product line or segment)
│ └── Competitive Intelligence
├── Field Marketing
│ ├── Events
│ └── Regional / ABM
└── Brand & Communications
├── Brand Design
└── PR / Analyst Relations
```
**At this stage:**
- The CMO is a board-level communicator, not a campaign manager
- Each function has a dedicated leader (director or VP level)
- Marketing ops owns the attribution model and reports to CMO directly
- Analyst relations becomes important (Gartner, Forrester, G2 category positioning)
---
### Growth Stage ($75M+ ARR)
Marketing becomes a portfolio of specialized functions. Each major channel has a team. Brand is a serious investment. Analyst relations is a dedicated role. International marketing teams form.
The CMO's job shifts from building the machine to:
- Setting marketing strategy across a complex portfolio
- Representing marketing at the board level
- Owning brand and category leadership
- Cross-functional leadership with CRO, CPO, CEO
---
## 2. Hiring Sequence
### Who to Hire First
**The generalist content + demand gen marketer.**
Must-haves:
- Can write (blog posts, emails, landing pages — not just briefs)
- Can run paid campaigns (Google, LinkedIn — not just "I've managed agencies")
- Can operate a marketing automation platform (HubSpot, Marketo)
- Comfortable with data (can build a funnel report without asking an analyst)
This person builds the foundation. They're not a specialist yet — they're testing channels and building the process.
Avoid: Hiring a brand designer first. Or a community manager. Or a social media manager. These are specialties that compound on a foundation that doesn't exist yet.
### Who to Hire Second
**A specialist in the channel that's working.**
If organic search is your top lead source → hire an SEO/content lead.
If events are driving pipeline → hire a field marketer.
If outbound is working → hire an SDR manager or demand gen specialist.
Don't hire a generalist #2. By now you know what's working. Depth beats breadth.
### Who to Hire Third
**Product marketing.**
Why third and not first? Because PMM output (positioning, sales enablement, launch) is most valuable when there's an audience to position to and a sales team to enable. Before that, the founding marketer does "good enough" PMM work.
PMM hire profile: Has done positioning work before, has run a product launch, has built sales decks that sales actually uses, comfortable with win/loss analysis.
PMM:PM ratio benchmark: 1 PMM per 2–3 PMs. If you have 6 PMs and 1 PMM, you have a messaging and enablement problem.
### Who to Hire Fourth
**Marketing operations.**
This is consistently hired too late. By the time most companies hire marketing ops, attribution is broken, leads are being lost in handoffs, and the CRM data is unreliable. Hire marketing ops before you think you need it.
Marketing ops profile: HubSpot/Marketo certified, SQL capable, understands multi-touch attribution, has integrated CRM + sales engagement tools before.
### Hiring Decision Triggers
| Hire | Trigger |
|------|---------|
| Generalist marketer #1 | Sales motion is repeatable, need to scale lead generation |
| Specialist #2 | One channel is clearly outperforming — double down |
| Product marketer | Sales team is losing deals to positioning confusion or competitor gaps |
| Marketing ops | Running 3+ campaigns simultaneously with manual tracking |
| Field marketer | Events are in the strategy and attendance > 2 conferences/quarter |
| Head of Marketing / VP | Team is 3+ people and needs an org owner |
| CMO | Company is Series B/C and marketing needs board-level representation |
---
## 3. Agency vs. In-House
### Framework
Keep in-house what compounds. Outsource what's episodic or specialized.
| Function | Agency | In-House | Notes |
|----------|--------|----------|-------|
| Brand design | Early stage | Series B+ | Agency fine until redesigns become frequent |
| Paid media | < $50K/month spend | > $50K/month | Agency margin eats returns at scale |
| SEO strategy | Audit only | Ongoing execution | Strategy once, execution continuously |
| Content production | Overflow only | Core writers | Your voice must be yours |
| PR / comms | Almost always | $100M+ companies | Specialists required for media relationships |
| Marketing ops / CRM | Never | Always | This is your data infrastructure |
| Analyst relations | Initial strategy | Ongoing | Relationship-based — needs dedicated owner |
| Video / creative production | Always | Rarely | Episodic, specialized equipment |
### Agency Red Flags
- They want to own your ad accounts. (Always keep ownership. No exceptions.)
- SLA is "5 business days for creative requests." For a performance channel, that's too slow.
- Reporting is impressions, CPM, and "brand lift." Where's the pipeline?
- They can't tell you your CAC from their channel.
- They won't share the actual data — only their dashboard.
- Your account manager changes every 6 months.
### Agency Evaluation Criteria
1. **Proof of work in your category** — ask for 3 case studies with actual CAC and pipeline data
2. **Who actually does the work** — senior pitch team ≠ junior execution team
3. **Account ownership** — all accounts, pixels, analytics must be in your name
4. **Reporting cadence** — weekly data, monthly strategy, quarterly business review
5. **Exit terms** — how do you offboard without losing your data, accounts, and history?
---
## 4. Marketing Ops and Tech Stack
### The Minimum Viable Stack
| Layer | Tool | Purpose |
|-------|------|---------|
| CRM | HubSpot / Salesforce | Contact database, pipeline, source of truth |
| Marketing automation | HubSpot / Marketo / ActiveCampaign | Email, nurture, lead scoring |
| Analytics | Google Analytics 4 + Segment | Traffic, behavior, event tracking |
| Attribution | HubSpot / Attributer.io / Dreamdata | Multi-touch pipeline attribution |
| Paid | Google Ads + LinkedIn Ads | Performance channels |
| SEO | Ahrefs / Semrush | Keyword research, rank tracking |
| Chat/conversion | Intercom / Drift | In-product + website conversion |
**The integration that breaks most:** CRM ↔ Marketing automation ↔ Sales engagement. When these aren't synced properly, leads are lost, attribution is wrong, and marketing and sales fight about pipeline. Fix this first.
### Marketing Ops Ownership
Marketing ops must own:
- CRM data quality (field standardization, deduplication, routing)
- Lead scoring model (and quarterly review against conversion data)
- Attribution model (with documented assumptions)
- Campaign tracking (UTM governance — no UTM = no attribution)
- Tech stack evaluation and contracts
Marketing ops must NOT own:
- Strategy (they enable it, not set it)
- Content production
- Campaign creative
---
## 5. Cross-Functional Alignment
### Marketing + Sales
The most important cross-functional relationship in a SLG company. Where it breaks:
| Problem | Root Cause | Fix |
|---------|-----------|-----|
| "Marketing sends us bad leads" | MQL definition is unclear or wrong | Define MQL jointly, score against conversion data |
| "Sales doesn't follow up on leads" | No SLA, no consequence | Define SLA (e.g., 24-hour response), track in CRM |
| "Marketing doesn't understand what customers care about" | No win/loss sharing | Weekly call: sales shares 3 deal insights, marketing shares 3 content results |
| "We don't know what's working" | Attribution is broken | Marketing ops fixes attribution before next budget cycle |
**The SLA agreement (document this):**
- Marketing commits: X MQLs/week meeting defined criteria, 48-hour SLA from form fill to SDR outreach
- Sales commits: All MQLs contacted within 24 hours, disposition logged in CRM within 5 days
### Marketing + Product
Where it breaks and how to fix it:
| Problem | Fix |
|---------|-----|
| PMM learns about launches 2 weeks before ship | PMM joins the product planning process at the roadmap stage, not the sprint stage |
| Feature launches with no messaging | Launch tiers: Tier 1 (major, full launch), Tier 2 (minor, release notes + 1 post), Tier 3 (internal only) |
| Product doesn't use customer insights from marketing | Monthly session: PMM shares win/loss themes, competitive intel, ICP data |
| No feedback loop on messaging in-product | PMM owns in-product copy review, not just external comms |
### Marketing + Customer Success
Customer success is marketing's best source of truth:
- **ICP validation:** Which customers are expanding? Which are churning? This refines who you target.
- **Proof points:** CS-sourced case studies and testimonials outperform vendor-written content 3:1 in conversion.
- **Messaging test:** If CS is answering the same question 20 times, marketing hasn't explained it clearly enough.
- **Referral programs:** CS owns the relationship; marketing owns the mechanics. Design them together.
Cadence: Monthly meeting between CMO and VP/Head of CS. Agenda: retention trends, expansion patterns, at-risk customers, NPS themes.
FILE:scripts/growth_model_simulator.py
#!/usr/bin/env python3
"""
Growth Model Simulator
----------------------
Projects MRR growth across different growth models (PLG, sales-led, community-led,
hybrid) and shows the impact of channel mix changes on growth trajectory.
Usage:
python growth_model_simulator.py
Inputs (edit INPUTS section):
- Starting MRR and churn rate
- Current channel mix (% of new MRR from each source)
- Conversion rates per model
- Growth rate assumptions per channel
Outputs:
- 12-month MRR projection by growth model
- Channel mix impact analysis (what happens if you shift mix)
- Break-even months for each model
- Side-by-side comparison table
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------
@dataclass
class ChannelSource:
name: str
pct_of_new_mrr: float # Current share of new MRR (0.0–1.0)
monthly_growth_rate: float # How fast this channel grows month-over-month
cac: float # CAC in dollars
payback_months: float # Months to recover CAC
@dataclass
class GrowthModel:
name: str
description: str
channel_mix: Dict[str, float] # channel name → % of new MRR
new_mrr_monthly_base: float # Starting new MRR/month from this model
monthly_acceleration: float # Acceleration factor (compounding)
avg_ltv_cac: float # Expected LTV:CAC at scale
months_to_steady_state: int # Months before model hits its natural growth rate
notes: List[str] = field(default_factory=list)
@dataclass
class MonthSnapshot:
month: int
mrr: float
new_mrr: float
churned_mrr: float
expansion_mrr: float
net_new_mrr: float
cumulative_cac_spend: float
@dataclass
class ModelProjection:
model: GrowthModel
snapshots: List[MonthSnapshot]
break_even_month: Optional[int] # Month when cumulative revenue > cumulative CAC
# ---------------------------------------------------------------------------
# INPUTS — edit these
# ---------------------------------------------------------------------------
STARTING_MRR = 85_000 # Current MRR ($)
MONTHLY_CHURN_RATE = 0.012 # Monthly churn rate (1.2% = ~14% annual)
EXPANSION_RATE = 0.008 # Monthly expansion MRR as % of existing MRR
GROSS_MARGIN = 0.75
SIMULATION_MONTHS = 18
# Channel sources (used to model mix shift scenarios)
CHANNELS: List[ChannelSource] = [
ChannelSource("Organic/SEO", pct_of_new_mrr=0.28, monthly_growth_rate=0.04, cac=1_800, payback_months=9),
ChannelSource("PLG Self-Serve", pct_of_new_mrr=0.15, monthly_growth_rate=0.08, cac=900, payback_months=5),
ChannelSource("Outbound SDR", pct_of_new_mrr=0.25, monthly_growth_rate=0.02, cac=5_100, payback_months=21),
ChannelSource("Paid Search", pct_of_new_mrr=0.15, monthly_growth_rate=0.01, cac=6_200, payback_months=26),
ChannelSource("Events/Field", pct_of_new_mrr=0.08, monthly_growth_rate=0.01, cac=9_800, payback_months=41),
ChannelSource("Partner/Channel", pct_of_new_mrr=0.09, monthly_growth_rate=0.05, cac=3_400, payback_months=14),
]
# Growth models to simulate
GROWTH_MODELS: List[GrowthModel] = [
GrowthModel(
name="Current Mix",
description="Baseline — maintain current channel allocation",
channel_mix={"Organic/SEO": 0.28, "PLG Self-Serve": 0.15, "Outbound SDR": 0.25,
"Paid Search": 0.15, "Events/Field": 0.08, "Partner/Channel": 0.09},
new_mrr_monthly_base=12_000,
monthly_acceleration=0.025,
avg_ltv_cac=3.2,
months_to_steady_state=3,
notes=["Baseline. No changes to channel mix."],
),
GrowthModel(
name="PLG-First",
description="Shift budget toward PLG self-serve and organic; reduce paid and outbound",
channel_mix={"Organic/SEO": 0.35, "PLG Self-Serve": 0.35, "Outbound SDR": 0.10,
"Paid Search": 0.08, "Events/Field": 0.04, "Partner/Channel": 0.08},
new_mrr_monthly_base=9_500, # Slower start — PLG takes time to activate
monthly_acceleration=0.048, # But compounds faster
avg_ltv_cac=5.8,
months_to_steady_state=6, # PLG loops take time to build
notes=[
"Lower new MRR in months 1-6 while PLG loops activate.",
"Acceleration compounds strongly after month 6.",
"Requires product investment in activation/onboarding.",
"Best fit if time-to-value < 30 min and viral coefficient > 0.3.",
],
),
GrowthModel(
name="Sales-Led Scale",
description="Double down on outbound SDR and field; optimize for enterprise ACV",
channel_mix={"Organic/SEO": 0.20, "PLG Self-Serve": 0.05, "Outbound SDR": 0.40,
"Paid Search": 0.15, "Events/Field": 0.15, "Partner/Channel": 0.05},
new_mrr_monthly_base=15_000, # Higher new MRR from enterprise ACV
monthly_acceleration=0.018, # Linear growth — headcount-constrained
avg_ltv_cac=2.8,
months_to_steady_state=2,
notes=[
"Fastest short-term new MRR if ACV > $30K.",
"Growth is linear — adds headcount to add pipeline.",
"CAC and payback worsen as SDR market tightens.",
"Requires sales capacity increase to sustain.",
],
),
GrowthModel(
name="Community-Led",
description="Invest in community and content; reduce paid; long-term brand play",
channel_mix={"Organic/SEO": 0.45, "PLG Self-Serve": 0.15, "Outbound SDR": 0.15,
"Paid Search": 0.05, "Events/Field": 0.10, "Partner/Channel": 0.10},
new_mrr_monthly_base=7_000, # Slowest start
monthly_acceleration=0.038,
avg_ltv_cac=4.5,
months_to_steady_state=9, # Community takes longest to activate
notes=[
"Lowest new MRR in months 1-9.",
"Community trust drives lower CAC and higher retention at scale.",
"Best for categories where buyers seek peer validation.",
"Requires dedicated community manager from day one.",
],
),
GrowthModel(
name="Hybrid PLS",
description="PLG self-serve for SMB + sales-assisted for enterprise (Product-Led Sales)",
channel_mix={"Organic/SEO": 0.30, "PLG Self-Serve": 0.28, "Outbound SDR": 0.22,
"Paid Search": 0.08, "Events/Field": 0.06, "Partner/Channel": 0.06},
new_mrr_monthly_base=11_000,
monthly_acceleration=0.035,
avg_ltv_cac=4.1,
months_to_steady_state=4,
notes=[
"PLG handles SMB; sales closes enterprise with PQL signals.",
"Requires clear PQL definition and SDR/PLG handoff process.",
"Best if you have a product with both bottom-up and top-down adoption.",
],
),
]
# ---------------------------------------------------------------------------
# Simulation engine
# ---------------------------------------------------------------------------
def simulate_model(model: GrowthModel, months: int) -> ModelProjection:
snapshots: List[MonthSnapshot] = []
mrr = STARTING_MRR
cumulative_cac = 0.0
cumulative_revenue = 0.0
break_even_month = None
for m in range(1, months + 1):
# Ramp up — new_mrr accelerates each month
if m <= model.months_to_steady_state:
# Ramp phase: linear ramp from 60% to 100% of base
ramp_factor = 0.6 + 0.4 * (m / model.months_to_steady_state)
else:
# Steady state: compound acceleration
months_past_ramp = m - model.months_to_steady_state
ramp_factor = 1.0 + model.monthly_acceleration * months_past_ramp
new_mrr = model.new_mrr_monthly_base * ramp_factor
churned_mrr = mrr * MONTHLY_CHURN_RATE
expansion_mrr = mrr * EXPANSION_RATE
net_new_mrr = new_mrr - churned_mrr + expansion_mrr
mrr = mrr + net_new_mrr
# CAC spend approximation: new_mrr / (avg_deal_mrr) * blended_cac
# Use weighted CAC from channel mix
weighted_cac = _weighted_cac(model.channel_mix)
avg_deal_mrr = 1_500 # Assumption: $1,500 average deal MRR
deals_this_month = new_mrr / avg_deal_mrr
cac_spend = deals_this_month * weighted_cac
cumulative_cac += cac_spend
cumulative_revenue += mrr * GROSS_MARGIN
if break_even_month is None and cumulative_revenue >= cumulative_cac:
break_even_month = m
snapshots.append(MonthSnapshot(
month=m,
mrr=mrr,
new_mrr=new_mrr,
churned_mrr=churned_mrr,
expansion_mrr=expansion_mrr,
net_new_mrr=net_new_mrr,
cumulative_cac_spend=cumulative_cac,
))
return ModelProjection(
model=model,
snapshots=snapshots,
break_even_month=break_even_month,
)
def _weighted_cac(channel_mix: Dict[str, float]) -> float:
channel_cac = {ch.name: ch.cac for ch in CHANNELS}
total = sum(
channel_mix.get(name, 0) * cac
for name, cac in channel_cac.items()
)
weight_sum = sum(channel_mix.values())
return total / weight_sum if weight_sum > 0 else 5_000
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt_mrr(n: float) -> str:
if n >= 1_000_000:
return f".3fM"
return f".1fK"
def fmt_currency(n: float) -> str:
if n >= 1_000_000:
return f".2fM"
if n >= 1_000:
return f".1fK"
return f".0f"
def print_header(title: str) -> None:
width = 78
print("\n" + "=" * width)
print(f" {title}")
print("=" * width)
def print_channel_overview() -> None:
print_header("Current Channel Mix")
print(f" Starting MRR: {fmt_mrr(STARTING_MRR)} | Monthly churn: {MONTHLY_CHURN_RATE:.1%} | Expansion: {EXPANSION_RATE:.1%}/mo")
print()
print(f" {'Channel':<22} {'% MRR':>7} {'CAC':>8} {'Payback':>9} {'Growth/mo':>10}")
print(" " + "-" * 60)
for ch in sorted(CHANNELS, key=lambda c: c.pct_of_new_mrr, reverse=True):
print(
f" {ch.name:<22} {ch.pct_of_new_mrr:>6.0%} "
f"{fmt_currency(ch.cac):>8} {ch.payback_months:>7.0f}mo "
f"{ch.monthly_growth_rate:>9.1%}"
)
def print_model_detail(proj: ModelProjection) -> None:
model = proj.model
print_header(f"Model: {model.name}")
print(f" {model.description}")
if model.notes:
print()
for note in model.notes:
print(f" • {note}")
print()
# Print monthly snapshot (every 3 months + final)
milestones = set(range(3, SIMULATION_MONTHS + 1, 3)) | {SIMULATION_MONTHS}
print(f" {'Month':<7} {'MRR':>10} {'New MRR':>9} {'Churned':>9} {'Expand':>8} {'Net New':>9}")
print(" " + "-" * 56)
for snap in proj.snapshots:
if snap.month in milestones:
print(
f" {snap.month:<7} {fmt_mrr(snap.mrr):>10} "
f"{fmt_mrr(snap.new_mrr):>9} {fmt_mrr(snap.churned_mrr):>9} "
f"{fmt_mrr(snap.expansion_mrr):>8} {fmt_mrr(snap.net_new_mrr):>9}"
)
final = proj.snapshots[-1]
growth_x = final.mrr / STARTING_MRR
arr_final = final.mrr * 12
weighted_cac = _weighted_cac(model.channel_mix)
be = f"Month {proj.break_even_month}" if proj.break_even_month else f"> {SIMULATION_MONTHS}mo"
print()
print(f" Final MRR ({SIMULATION_MONTHS}mo): {fmt_mrr(final.mrr)}")
print(f" Final ARR: {fmt_currency(arr_final)}")
print(f" Growth multiple: {growth_x:.1f}x from starting MRR")
print(f" Weighted blended CAC: {fmt_currency(weighted_cac)}")
print(f" Expected LTV:CAC: {model.avg_ltv_cac:.1f}x")
print(f" Months to steady state:{model.months_to_steady_state}")
print(f" CAC break-even: {be}")
def print_comparison_table(projections: List[ModelProjection]) -> None:
print_header(f"Growth Model Comparison — Month {SIMULATION_MONTHS} Outcomes")
header = (
f" {'Model':<20} {'MRR (final)':>12} {'ARR (final)':>12} "
f"{'Growth':>7} {'LTV:CAC':>8} {'Break-even':>11}"
)
print(header)
print(" " + "-" * 74)
for proj in sorted(projections, key=lambda p: p.snapshots[-1].mrr, reverse=True):
final = proj.snapshots[-1]
growth_x = final.mrr / STARTING_MRR
arr_final = final.mrr * 12
be = f"Mo {proj.break_even_month}" if proj.break_even_month else f">{SIMULATION_MONTHS}mo"
print(
f" {proj.model.name:<20} {fmt_mrr(final.mrr):>12} "
f"{fmt_currency(arr_final):>12} {growth_x:>6.1f}x "
f"{proj.model.avg_ltv_cac:>7.1f}x {be:>11}"
)
def print_channel_mix_impact(projections: List[ModelProjection]) -> None:
print_header("Channel Mix Impact Analysis")
print(" How shifting channel mix changes growth trajectory:\n")
baseline = next((p for p in projections if p.model.name == "Current Mix"), None)
if not baseline:
return
baseline_final_mrr = baseline.snapshots[-1].mrr
for proj in projections:
if proj.model.name == "Current Mix":
continue
final_mrr = proj.snapshots[-1].mrr
delta = final_mrr - baseline_final_mrr
delta_pct = (delta / baseline_final_mrr) * 100
arrow = "↑" if delta > 0 else "↓"
m6_mrr = proj.snapshots[5].mrr if len(proj.snapshots) >= 6 else 0
m6_baseline = baseline.snapshots[5].mrr if len(baseline.snapshots) >= 6 else 0
m6_delta = m6_mrr - m6_baseline
m6_pct = (m6_delta / m6_baseline) * 100 if m6_baseline else 0
m6_arrow = "↑" if m6_delta > 0 else "↓"
print(f" {proj.model.name}:")
print(f" Month 6: {m6_arrow} {abs(m6_pct):.1f}% vs. current ({fmt_mrr(m6_delta)} {'more' if m6_delta > 0 else 'less'} MRR)")
print(f" Month {SIMULATION_MONTHS}: {arrow} {abs(delta_pct):.1f}% vs. current ({fmt_mrr(delta)} {'more' if delta > 0 else 'less'} MRR)")
if proj.model.months_to_steady_state > 4:
print(f" ⚠ Model takes {proj.model.months_to_steady_state} months to reach steady state — short-term dip expected.")
print()
def print_decision_guide(projections: List[ModelProjection]) -> None:
print_header("Decision Guide")
print(" Choose your growth model based on your constraints:\n")
guides = [
("ACV < $5K and fast time-to-value", "PLG-First"),
("ACV > $25K and complex buying process", "Sales-Led Scale"),
("Strong practitioner community exists", "Community-Led"),
("Both SMB self-serve and enterprise buyers", "Hybrid PLS"),
("Uncertain — keep optionality", "Current Mix"),
]
for condition, model_name in guides:
proj = next((p for p in projections if p.model.name == model_name), None)
if proj:
final_mrr = proj.snapshots[-1].mrr
print(f" If: {condition}")
print(f" → Use {model_name} → {fmt_mrr(final_mrr)} MRR at month {SIMULATION_MONTHS}")
print()
print(" Key question before switching models:")
print(" 'Do we have 12-18 months of runway to prove the new model")
print(" while the current model continues in parallel?'")
print(" If no → optimize current model. Don't switch.")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
print_channel_overview()
projections = [simulate_model(model, SIMULATION_MONTHS) for model in GROWTH_MODELS]
for proj in projections:
print_model_detail(proj)
print_comparison_table(projections)
print_channel_mix_impact(projections)
print_decision_guide(projections)
print("\n" + "=" * 78)
print(" Notes:")
print(f" Starting MRR: {fmt_mrr(STARTING_MRR)}")
print(f" Simulation: {SIMULATION_MONTHS} months")
print(f" Churn: {MONTHLY_CHURN_RATE:.1%}/mo ({MONTHLY_CHURN_RATE*12:.0%} annualized)")
print(f" Expansion: {EXPANSION_RATE:.1%}/mo of existing MRR")
print(f" Gross margin: {GROSS_MARGIN:.0%}")
print(" Acceleration rates are estimates — validate against your actuals.")
print("=" * 78 + "\n")
if __name__ == "__main__":
main()
FILE:scripts/marketing_budget_modeler.py
#!/usr/bin/env python3
"""
Marketing Budget Modeler
------------------------
Allocates marketing budget across channels based on CAC efficiency and
target MQL volume. Models conservative / moderate / aggressive scenarios.
Usage:
python marketing_budget_modeler.py
Inputs (edit INPUTS section below or extend with argparse):
- Annual revenue target (new ARR)
- Average selling price (ASP)
- Conversion rates by funnel stage
- Historical CAC per channel
- Channel capacity constraints (max MQLs the channel can realistically produce)
Outputs:
- Required MQL volume by channel
- Budget allocation per channel per scenario
- LTV:CAC and payback period per channel
- Summary table across scenarios
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import Dict, List, Tuple
# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------
@dataclass
class Channel:
name: str
cac: float # Customer acquisition cost ($)
max_mqls_per_month: int # Realistic capacity ceiling (MQLs/month)
mql_to_close_rate: float # Combined MQL → closed-won rate (0.0–1.0)
payback_months: float # Based on ARPU × gross margin
ltv: float # Lifetime value ($)
trend: str = "stable" # "improving" | "stable" | "declining"
@dataclass
class FunnelRates:
mql_to_sal: float # MQL → Sales Accepted Lead
sal_to_sql: float # SAL → Sales Qualified Lead
sql_to_opp: float # SQL → Opportunity
opp_to_close: float # Opportunity → Closed-Won
@property
def mql_to_close(self) -> float:
return self.mql_to_sal * self.sal_to_sql * self.sql_to_opp * self.opp_to_close
@dataclass
class ScenarioResult:
name: str
total_budget: float
channel_budgets: Dict[str, float]
channel_mqls: Dict[str, int]
projected_customers: int
projected_arr: float
blended_cac: float
notes: List[str] = field(default_factory=list)
# ---------------------------------------------------------------------------
# INPUTS — edit these
# ---------------------------------------------------------------------------
TARGET_NEW_ARR = 3_000_000 # New ARR to generate this year ($)
ASP_ANNUAL = 18_000 # Average annual contract value ($)
GROSS_MARGIN = 0.75 # Product gross margin (%)
ARPU_MONTHLY = ASP_ANNUAL / 12 # Monthly revenue per account
FUNNEL = FunnelRates(
mql_to_sal=0.65,
sal_to_sql=0.45,
sql_to_opp=0.75,
opp_to_close=0.27,
)
# LTV = ARPU_monthly × gross_margin / monthly_churn_rate
MONTHLY_CHURN = 0.012 # ~14% annual churn
LTV = (ARPU_MONTHLY * GROSS_MARGIN) / MONTHLY_CHURN
CHANNELS: List[Channel] = [
Channel(
name="Organic SEO",
cac=1_800,
max_mqls_per_month=80,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(1_800 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="improving",
),
Channel(
name="Paid Search",
cac=6_200,
max_mqls_per_month=60,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(6_200 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="stable",
),
Channel(
name="Paid Social (LinkedIn)",
cac=8_500,
max_mqls_per_month=35,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(8_500 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="declining",
),
Channel(
name="Outbound SDR",
cac=5_100,
max_mqls_per_month=50,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(5_100 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="stable",
),
Channel(
name="Events / Field",
cac=9_800,
max_mqls_per_month=25,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(9_800 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="stable",
),
Channel(
name="Partner / Channel",
cac=3_400,
max_mqls_per_month=30,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(3_400 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="improving",
),
Channel(
name="Content / Inbound",
cac=2_600,
max_mqls_per_month=45,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(2_600 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="improving",
),
]
# ---------------------------------------------------------------------------
# Core calculations
# ---------------------------------------------------------------------------
def customers_needed(target_arr: float, asp: float) -> int:
return math.ceil(target_arr / asp)
def mqls_needed_total(customers: int, mql_to_close: float) -> int:
return math.ceil(customers / mql_to_close)
def ltv_to_cac(ltv: float, cac: float) -> float:
return ltv / cac if cac > 0 else 0.0
def score_channel(ch: Channel) -> float:
"""
Score a channel for budget priority.
Higher = more efficient. Used to rank allocation order.
Factors: LTV:CAC ratio, trend multiplier, capacity.
"""
ratio = ltv_to_cac(ch.ltv, ch.cac)
trend_mult = {"improving": 1.2, "stable": 1.0, "declining": 0.7}.get(ch.trend, 1.0)
return ratio * trend_mult
def allocate_mqls(
channels: List[Channel],
total_mqls_needed: int,
budget_multiplier: float = 1.0,
) -> Tuple[Dict[str, int], Dict[str, float]]:
"""
Allocate MQL targets across channels in priority order (best LTV:CAC first).
budget_multiplier: 0.7 = conservative, 1.0 = moderate, 1.3 = aggressive.
Returns (channel → MQLs, channel → budget).
"""
ranked = sorted(channels, key=score_channel, reverse=True)
remaining = total_mqls_needed
channel_mqls: Dict[str, int] = {}
channel_budget: Dict[str, float] = {}
for ch in ranked:
if remaining <= 0:
channel_mqls[ch.name] = 0
channel_budget[ch.name] = 0.0
continue
# Apply capacity ceiling scaled by multiplier (aggressive = push capacity)
capacity = int(ch.max_mqls_per_month * 12 * budget_multiplier)
allocated = min(remaining, capacity)
channel_mqls[ch.name] = allocated
channel_budget[ch.name] = allocated * ch.cac
remaining -= allocated
return channel_mqls, channel_budget
def build_scenario(
name: str,
channels: List[Channel],
total_mqls: int,
multiplier: float,
notes: List[str],
) -> ScenarioResult:
channel_mqls, channel_budget = allocate_mqls(channels, total_mqls, multiplier)
total_budget = sum(channel_budget.values())
total_mqls_allocated = sum(channel_mqls.values())
projected_customers = math.floor(total_mqls_allocated * FUNNEL.mql_to_close)
projected_arr = projected_customers * ASP_ANNUAL
# Blended CAC = total budget / customers acquired
blended_cac = total_budget / projected_customers if projected_customers > 0 else 0.0
return ScenarioResult(
name=name,
total_budget=total_budget,
channel_budgets=channel_budget,
channel_mqls=channel_mqls,
projected_customers=projected_customers,
projected_arr=projected_arr,
blended_cac=blended_cac,
notes=notes,
)
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt_currency(n: float) -> str:
if n >= 1_000_000:
return f".2fM"
if n >= 1_000:
return f".1fK"
return f".0f"
def fmt_ratio(n: float) -> str:
return f"{n:.1f}x"
def print_header(title: str) -> None:
width = 72
print("\n" + "=" * width)
print(f" {title}")
print("=" * width)
def print_channel_table(channels: List[Channel]) -> None:
print_header("Channel Analysis — Current State")
header = f"{'Channel':<25} {'CAC':>8} {'Payback':>9} {'LTV:CAC':>8} {'Cap/mo':>7} {'Trend':>10}"
print(header)
print("-" * 72)
for ch in sorted(channels, key=score_channel, reverse=True):
ratio = ltv_to_cac(ch.ltv, ch.cac)
flag = ""
if ratio < 1:
flag = " ⚠ LOSS"
elif ratio >= 6:
flag = " ★ STRONG"
elif ratio >= 3:
flag = " ✓"
print(
f"{ch.name:<25} {fmt_currency(ch.cac):>8} "
f"{ch.payback_months:>7.1f}mo {fmt_ratio(ratio):>8} "
f"{ch.max_mqls_per_month:>7} {ch.trend:>10}{flag}"
)
def print_funnel_summary(customers: int, mqls: int) -> None:
print_header("Funnel Requirements")
print(f" Target new ARR: {fmt_currency(TARGET_NEW_ARR)}")
print(f" Average selling price: {fmt_currency(ASP_ANNUAL)}")
print(f" New customers needed: {customers}")
print(f" Funnel MQL→Close rate: {FUNNEL.mql_to_close:.1%}")
print(f" Total MQLs needed: {mqls}")
print(f"\n Funnel stage rates:")
print(f" MQL → SAL: {FUNNEL.mql_to_sal:.0%}")
print(f" SAL → SQL: {FUNNEL.mql_to_sal * FUNNEL.sal_to_sql:.0%}")
print(f" SQL → Opportunity: {FUNNEL.mql_to_sal * FUNNEL.sal_to_sql * FUNNEL.sql_to_opp:.0%}")
print(f" Opportunity → Close: {FUNNEL.mql_to_close:.0%}")
print(f"\n LTV (estimated): {fmt_currency(LTV)}")
print(f" Monthly churn: {MONTHLY_CHURN:.1%} ({MONTHLY_CHURN*12:.0%} annualized)")
def print_scenario(result: ScenarioResult, channels: List[Channel]) -> None:
print_header(f"Scenario: {result.name}")
print(f" Total marketing budget: {fmt_currency(result.total_budget)}")
print(f" Projected customers: {result.projected_customers}")
print(f" Projected new ARR: {fmt_currency(result.projected_arr)}")
print(f" Blended CAC: {fmt_currency(result.blended_cac)}")
blended_ltv_cac = LTV / result.blended_cac if result.blended_cac > 0 else 0
blended_payback = result.blended_cac / (ARPU_MONTHLY * GROSS_MARGIN)
print(f" Blended LTV:CAC: {fmt_ratio(blended_ltv_cac)}", end="")
if blended_ltv_cac < 1:
print(" ⚠ BELOW BREAK-EVEN")
elif blended_ltv_cac < 3:
print(" △ MARGINAL")
elif blended_ltv_cac >= 3:
print(" ✓ HEALTHY")
else:
print()
print(f" Blended payback: {blended_payback:.1f} months")
if result.notes:
print(f"\n Notes:")
for note in result.notes:
print(f" • {note}")
print(f"\n {'Channel':<25} {'MQLs':>6} {'Budget':>10} {'% of Budget':>12} {'LTV:CAC':>8}")
print(" " + "-" * 65)
for ch in sorted(channels, key=score_channel, reverse=True):
mqls = result.channel_mqls.get(ch.name, 0)
budget = result.channel_budgets.get(ch.name, 0.0)
pct = (budget / result.total_budget * 100) if result.total_budget > 0 else 0
ratio = ltv_to_cac(ch.ltv, ch.cac)
print(
f" {ch.name:<25} {mqls:>6} {fmt_currency(budget):>10} "
f"{pct:>11.1f}% {fmt_ratio(ratio):>8}"
)
def print_scenario_comparison(scenarios: List[ScenarioResult]) -> None:
print_header("Scenario Comparison")
header = f"{'Scenario':<18} {'Budget':>10} {'Customers':>10} {'ARR':>10} {'Blended CAC':>12} {'LTV:CAC':>8} {'Payback':>9}"
print(header)
print("-" * 82)
for s in scenarios:
blended_ltv_cac = LTV / s.blended_cac if s.blended_cac > 0 else 0
blended_payback = s.blended_cac / (ARPU_MONTHLY * GROSS_MARGIN)
print(
f"{s.name:<18} {fmt_currency(s.total_budget):>10} "
f"{s.projected_customers:>10} {fmt_currency(s.projected_arr):>10} "
f"{fmt_currency(s.blended_cac):>12} {fmt_ratio(blended_ltv_cac):>8} "
f"{blended_payback:>7.1f}mo"
)
def print_recommendations(channels: List[Channel]) -> None:
print_header("Channel Recommendations")
scale = [ch for ch in channels if score_channel(ch) >= 1.5 and ch.trend in ("improving", "stable")]
hold = [ch for ch in channels if 0.8 <= score_channel(ch) < 1.5 or (ch.trend == "stable" and ltv_to_cac(ch.ltv, ch.cac) >= 3)]
cut = [ch for ch in channels if ltv_to_cac(ch.ltv, ch.cac) < 2 or ch.trend == "declining"]
# Deduplicate
hold = [ch for ch in hold if ch not in scale]
cut = [ch for ch in cut if ch not in scale and ch not in hold]
if scale:
print(" SCALE (strong LTV:CAC, improving or stable trend):")
for ch in scale:
print(f" + {ch.name} [LTV:CAC {fmt_ratio(ltv_to_cac(ch.ltv, ch.cac))}, payback {ch.payback_months:.0f}mo]")
if hold:
print(" HOLD (monitor — adequate but not outstanding):")
for ch in hold:
print(f" = {ch.name} [LTV:CAC {fmt_ratio(ltv_to_cac(ch.ltv, ch.cac))}, trend: {ch.trend}]")
if cut:
print(" CUT or REDUCE (poor LTV:CAC or declining):")
for ch in cut:
print(f" - {ch.name} [LTV:CAC {fmt_ratio(ltv_to_cac(ch.ltv, ch.cac))}, trend: {ch.trend}]")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
customers = customers_needed(TARGET_NEW_ARR, ASP_ANNUAL)
total_mqls = mqls_needed_total(customers, FUNNEL.mql_to_close)
print_channel_table(CHANNELS)
print_funnel_summary(customers, total_mqls)
scenarios = [
build_scenario(
name="Conservative",
channels=CHANNELS,
total_mqls=total_mqls,
multiplier=0.7,
notes=[
"Prioritizes lowest CAC channels only.",
"May not reach MQL target — expect ~70% of goal.",
"Best for capital-constrained orgs or short runway.",
],
),
build_scenario(
name="Moderate",
channels=CHANNELS,
total_mqls=total_mqls,
multiplier=1.0,
notes=[
"Balanced allocation — efficiency-first but full MQL target.",
"Recommended baseline. Revisit Q2 based on actuals.",
],
),
build_scenario(
name="Aggressive",
channels=CHANNELS,
total_mqls=total_mqls,
multiplier=1.4,
notes=[
"Pushes all channels toward capacity ceiling.",
"Higher spend on lower-efficiency channels to hit volume.",
"Requires > 18-month runway to justify payback period.",
],
),
]
for scenario in scenarios:
print_scenario(scenario, CHANNELS)
print_scenario_comparison(scenarios)
print_recommendations(CHANNELS)
print("\n" + "=" * 72)
print(" Key questions before finalizing budget:")
print(" 1. What is the payback period the CFO/board will accept?")
print(" 2. Is CAC for declining-trend channels actually recoverable?")
print(" 3. Does the moderate scenario require sales headcount increase?")
print(" 4. Which channels have capacity to absorb 20% more spend?")
print("=" * 72 + "\n")
if __name__ == "__main__":
main()
Product leadership for scaling companies. Product vision, portfolio strategy, product-market fit, and product org design. Use when setting product vision, ma...
---
name: "cpo-advisor"
description: "Product leadership for scaling companies. Product vision, portfolio strategy, product-market fit, and product org design. Use when setting product vision, managing a product portfolio, measuring PMF, designing product teams, prioritizing at the portfolio level, reporting to the board on product, or when user mentions CPO, product strategy, product-market fit, product organization, portfolio prioritization, or roadmap strategy."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: cpo-leadership
updated: 2026-03-05
python-tools: pmf_scorer.py, portfolio_analyzer.py
frameworks: pmf-playbook, product-strategy, product-org-design
---
# CPO Advisor
Strategic product leadership. Vision, portfolio, PMF, org design. Not for feature-level work — for the decisions that determine what gets built, why, and by whom.
## Keywords
CPO, chief product officer, product strategy, product vision, product-market fit, PMF, portfolio management, product org, roadmap strategy, product metrics, north star metric, retention curve, product trio, team topologies, Jobs to be Done, category design, product positioning, board product reporting, invest-maintain-kill, BCG matrix, switching costs, network effects
## Quick Start
### Score Your Product-Market Fit
```bash
python scripts/pmf_scorer.py
```
Multi-dimensional PMF score across retention, engagement, satisfaction, and growth.
### Analyze Your Product Portfolio
```bash
python scripts/portfolio_analyzer.py
```
BCG matrix classification, investment recommendations, portfolio health score.
## The CPO's Core Responsibilities
The CPO owns three things. Everything else is delegation.
| Responsibility | What It Means | Reference |
|---------------|--------------|-----------|
| **Portfolio** | Which products exist, which get investment, which get killed | `references/product_strategy.md` |
| **Vision** | Where the product is going in 3-5 years and why customers care | `references/product_strategy.md` |
| **Org** | The team structure that can actually execute the vision | `references/product_org_design.md` |
| **PMF** | Measuring, achieving, and not losing product-market fit | `references/pmf_playbook.md` |
| **Metrics** | North star → leading → lagging hierarchy, board reporting | This file |
## Diagnostic Questions
These questions expose whether you have a strategy or a list.
**Portfolio:**
- Which product is the dog? Are you killing it or lying to yourself?
- If you had to cut 30% of your portfolio tomorrow, what stays?
- What's your portfolio's combined D30 retention? Is it trending up?
**PMF:**
- What's your retention curve for your best cohort?
- What % of users would be "very disappointed" if your product disappeared?
- Is organic growth happening without you pushing it?
**Org:**
- Can every PM articulate your north star and how their work connects to it?
- When did your last product trio do user interviews together?
- What's blocking your slowest team — the people or the structure?
**Strategy:**
- If you could only ship one thing this quarter, what is it and why?
- What's your moat in 12 months? In 3 years?
- What's the riskiest assumption in your current product strategy?
## Product Metrics Hierarchy
```
North Star Metric (1, owned by CPO)
↓ explains changes in
Leading Indicators (3-5, owned by PMs)
↓ eventually become
Lagging Indicators (revenue, churn, NPS)
```
**North Star rules:** One number. Measures customer value delivered, not revenue. Every team can influence it.
**Good North Stars by business model:**
| Model | North Star Example |
|-------|------------------|
| B2B SaaS | Weekly active accounts using core feature |
| Consumer | D30 retained users |
| Marketplace | Successful transactions per week |
| PLG | Accounts reaching "aha moment" within 14 days |
| Data product | Queries run per active user per week |
### The CPO Dashboard
| Category | Metric | Frequency |
|----------|--------|-----------|
| Growth | North star metric | Weekly |
| Growth | D30 / D90 retention by cohort | Weekly |
| Acquisition | New activations | Weekly |
| Activation | Time to "aha moment" | Weekly |
| Engagement | DAU/MAU ratio | Weekly |
| Satisfaction | NPS trend | Monthly |
| Portfolio | Revenue per product | Monthly |
| Portfolio | Engineering investment % per product | Monthly |
| Moat | Feature adoption depth | Monthly |
## Investment Postures
Every product gets one: **Invest / Maintain / Kill**. "Wait and see" is not a posture — it's a decision to lose share.
| Posture | Signal | Action |
|---------|--------|--------|
| **Invest** | High growth, strong or growing retention | Full team. Aggressive roadmap. |
| **Maintain** | Stable revenue, slow growth, good margins | Bug fixes only. Milk it. |
| **Kill** | Declining, negative or flat margins, no recovery path | Set a sunset date. Write a migration plan. |
## Red Flags
**Portfolio:**
- Products that have been "question marks" for 2+ quarters without a decision
- Engineering capacity allocated to your highest-revenue product but your highest-growth product is understaffed
- More than 30% of team time on products with declining revenue
**PMF:**
- You have to convince users to keep using the product
- Support requests are mostly "how do I do X" rather than "I want X to also do Y"
- D30 retention is below 20% (consumer) or 40% (B2B) and not improving
**Org:**
- PMs writing specs and handing to design, who hands to engineering (waterfall in agile clothing)
- Platform team has a 6-week queue for stream-aligned team requests
- CPO has not talked to a real customer in 30+ days
**Metrics:**
- North star going up while retention is going down (metric is wrong)
- Teams optimizing their own metrics at the expense of company metrics
- Roadmap built from sales requests, not user behavior data
## Integration with Other C-Suite Roles
| When... | CPO works with... | To... |
|---------|-------------------|-------|
| Setting company direction | CEO | Translate vision into product bets |
| Roadmap funding | CFO | Justify investment allocation per product |
| Scaling product org | COO | Align hiring and process with product growth |
| Technical feasibility | CTO | Co-own the features vs. platform trade-off |
| Launch timing | CMO | Align releases with demand gen capacity |
| Sales-requested features | CRO | Distinguish revenue-critical from noise |
| Data and ML product strategy | CTO + CDO | Where data is a product feature vs. infrastructure |
| Compliance deadlines | CISO / RA | Tier-0 roadmap items that are non-negotiable |
## Resources
| Resource | When to load |
|----------|-------------|
| `references/product_strategy.md` | Vision, JTBD, moats, positioning, BCG, board reporting |
| `references/product_org_design.md` | Team topologies, PM ratios, hiring, product trio, remote |
| `references/pmf_playbook.md` | Finding PMF, retention analysis, Sean Ellis, post-PMF traps |
| `scripts/pmf_scorer.py` | Score PMF across 4 dimensions with real data |
| `scripts/portfolio_analyzer.py` | BCG classify and score your product portfolio |
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- Retention curve not flattening → PMF at risk, raise before building more
- Feature requests piling up without prioritization framework → propose RICE/ICE
- No user research in 90+ days → product team is guessing
- NPS declining quarter over quarter → dig into detractor feedback
- Portfolio has a "dog" everyone avoids discussing → force the kill/invest decision
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Do we have PMF?" | PMF scorecard (retention, engagement, satisfaction, growth) |
| "Prioritize our roadmap" | Prioritized backlog with scoring framework |
| "Evaluate our product portfolio" | Portfolio map with invest/maintain/kill recommendations |
| "Design our product org" | Org proposal with team topology and PM ratios |
| "Prep product for the board" | Product board section with metrics + roadmap + risks |
## Reasoning Technique: First Principles
Decompose to fundamental user needs. Question every assumption about what customers want. Rebuild from validated evidence, not inherited roadmaps.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:references/pmf_playbook.md
# PMF Playbook
How to find product-market fit, measure it, and not lose it. Steps, not theory.
---
## What PMF Actually Is
PMF is when a product pulls users in rather than pushing them. Signals:
- Users find the product without you telling them about it
- They're upset when it doesn't work
- They bring their colleagues, their friends, their boss
- They build workarounds when a feature is missing
PMF is not:
- Users saying they like it
- A good NPS score with flat growth
- Enterprise customers who are locked in but churning at contract end
---
## Step 1: Find Your Best Customers First
Before measuring PMF across everyone, find the segment where PMF is strongest.
**How:**
1. Export a list of all churned users and all retained users (D90+)
2. Identify 5-10 attributes to compare: company size, industry, job title, signup source, first action taken, time to first value
3. Find the attributes that are over-represented in retained vs. churned
4. That's your highest-PMF segment
**This is not an analytics project.** Call 10 retained power users. Ask:
- "What were you doing before you found us?"
- "What would you use if we shut down tomorrow?"
- "Who else in your life has this problem?"
The segment where this conversation is easy and the answers are specific — that's where your PMF is.
---
## Step 2: Measure the Three PMF Signals
Run all three. They measure different things. One signal without the others is misleading.
### Signal 1: Retention Curves
**Method:**
1. Cohort users by week or month of first use
2. Calculate % still active at D1, D7, D14, D30, D60, D90
3. Plot the curve for each cohort
**Interpretation:**
| Curve Shape | What It Means |
|-------------|--------------|
| Drops to zero | No PMF. Product doesn't solve a recurring problem. |
| Drops and keeps dropping | Weak PMF. Some people find value, but not enough to keep coming back. |
| Drops then flattens above 0 | PMF signal. A core group finds ongoing value. |
| Flattens higher with each newer cohort | PMF improving. You're learning. |
**Benchmarks:**
| Segment | D30 Retention (PMF threshold) | D90 Retention (strong PMF) |
|---------|-------------------------------|---------------------------|
| Consumer | > 20% | > 10% |
| SMB SaaS | > 40% | > 25% |
| Enterprise SaaS | > 60% | > 45% |
| Marketplace (buyers) | > 30% | > 20% |
| PLG (free-to-paid) | > 25% free D30, > 50% paid D30 | > 15% free D90 |
**If retention is below threshold:**
- Don't run more acquisition. You'll just churn faster.
- Find the users who ARE retained. Understand why. Build for them.
---
### Signal 2: Sean Ellis Test
Survey users with one question: "How would you feel if you could no longer use [Product]?"
**Answers:**
- Very disappointed
- Somewhat disappointed
- Not disappointed (it really isn't that useful)
- N/A — I no longer use [Product]
**Scoring:**
- Count only "very disappointed" responses
- Divide by total non-churned respondents
- PMF threshold: **> 40% "very disappointed"**
**Sample size requirement:** Minimum 40 responses. Under 40, the signal is noisy.
**When to run it:**
- When you have 100-500 active users
- Quarterly for ongoing tracking
- After major product changes
**What to do with "somewhat disappointed":**
Don't lump them with "very disappointed." The delta between "somewhat" and "very" is where your retention problem lives. Interview people in the "somewhat" group. What's missing? Why only somewhat?
**When score is 20-35%:** You have a segment with PMF. Find them. Ask what they love. Run a separate survey for just that segment.
**When score is < 20%:** Your core value proposition isn't working. This is not a retention tactics problem. Revisit the fundamental problem you're solving.
---
### Signal 3: Organic Growth and Referral
**Metric:** % of new signups that came from existing user referral, word of mouth, or organic search — without a paid incentive.
**Threshold:** > 20% of new users are coming organically without incentive programs.
**How to measure:**
1. Tag signup source: paid, organic search, referral (with referral code), direct/dark social
2. Track monthly. Is the organic % trending up or stable?
3. Interview organic signups: "How did you hear about us?" (don't trust the dropdown)
**Why this matters:** Paid growth can mask the absence of PMF. You can buy users who churn. You can't buy users who tell their friends.
---
## Step 3: Run PMF Experiments (Pre-PMF)
If you're below thresholds, don't optimize — experiment. The goal is to find the version of the product where at least a small segment has PMF.
### The PMF Experiment Loop
```
1. Pick one customer segment + one hypothesis about their job to be done
2. Remove everything from the product that doesn't serve that job
3. Run a 4-week cohort with only that segment
4. Measure retention + Sean Ellis for that cohort
5. If PMF signal: this is your beachhead. Double down.
If no signal: new hypothesis. Repeat.
```
**Time box:** Each experiment 4-8 weeks. If you're running experiments for 18+ months with no signal, revisit the problem space, not just the solution.
### What to Change
| Lever | Change | Expected Impact |
|-------|--------|-----------------|
| Target segment | Narrow ICP from "all companies" to "Series A SaaS" | Faster learning, higher retention |
| Core job | Reframe from feature-benefit to outcome-benefit | Better product decisions |
| Onboarding | Remove steps to time-to-value | D1 retention up |
| Pricing | Move from per-seat to per-outcome | Align incentives with value |
| Channel | Switch from outbound to PLG | Different segment discovers product |
---
## Step 4: Validate PMF (Post-Signal, Pre-Scale)
Congratulations, you have a retention curve that flattens. Before you scale:
**Validate that it's real:**
- Can you acquire more of the same customers? (Test CAC at 2x current volume)
- Do the retained users expand? (Are they buying more seats, upgrading?)
- Is the NPS from retained users > 40?
- Are they forgiving of bugs and slowness? (Love, not tolerance)
**Validate the unit economics:**
- LTV / CAC > 3x (for SaaS)
- Payback period < 18 months
- Gross margin > 60% (SaaS), > 40% (marketplace)
**The danger zone:** Convincing yourself you have PMF before economics are viable. High retention with terrible unit economics is not a business — it's a hobby that grows.
---
## PMF by Business Model
### B2B SaaS
**Primary signal:** D90 retention > 45% in target segment.
**Secondary signals:**
- NPS from retained users > 50
- Expansion revenue from retained accounts (NRR > 110%)
- Sales cycle shortening as word-of-mouth increases
**PMF finding strategy:**
- Start with one vertical, not the whole market
- Get 3-5 reference customers who use it daily and refer others
- Don't expand segment until you can replicate the reference case
**Common false signals:**
- Retained users who are locked in by contract, not value
- Expansion revenue from upselling, not from organic growth
- High satisfaction survey scores with flat usage data
---
### B2C / Consumer
**Primary signal:** D30 retention > 20%, with a flat or rising tail at D90.
**Secondary signals:**
- DAU/MAU ratio > 20% (daily habit product: > 40%)
- Session depth (users exploring multiple features, not one-and-done)
- Organic referral rate > 20% of new installs
**PMF finding strategy:**
- Consumer PMF is about habit formation — which behavior do you own in a user's day?
- Find the "aha moment" (the action that predicts retention). Build everything to get users there faster.
- Segment ruthlessly — consumer PMF is often strong in one demographic, weak in others.
**Common false signals:**
- High D1 retention from email campaigns that re-engage dormant users
- Good NPS from vocal users who are power users, not typical users
- Media buzz driving installs from wrong audience
---
### Marketplace
**Primary signal:** Successful transaction rate and repeat buyer rate.
**Secondary signals:**
- Supply-side retention (sellers/providers coming back)
- Liquidity score: % of demand requests matched within acceptable time
- Referral: both sides sending others
**PMF challenge:** You have two customers (supply and demand). PMF can exist on one side and not the other.
**PMF finding strategy:**
- Start with constrained geography or category — don't try to be national before local works
- Measure GMV per cohort, not just transaction count
- Find the "magic moment" for both buyer and seller. Optimize for both.
---
### PLG (Product-Led Growth)
**Primary signal:** Free-to-paid conversion rate + paid retention.
**Secondary signals:**
- Time to activation (reaching the "aha moment" in free tier)
- PQL (product-qualified lead) conversion to paid
- Team invites from individual users (virality coefficient)
**PMF finding strategy:**
- The free tier must have genuine value — not a crippled trial
- Track activation milestone (the action that predicts conversion)
- Optimize activation before conversion — conversion optimizations don't work if nobody activates
---
## After PMF: The Scaling Trap
Most companies that fail after PMF weren't ready to scale. They scaled the wrong thing.
### The Scaling Trap
You have PMF with segment A. You hire sales and start selling to segment B. Segment B doesn't retain. NPS drops. Engineers chase segment B feature requests. Segment A users feel abandoned.
**This is the most common way early-stage companies die after PMF.**
### What to Do After PMF
**First 90 days after confirming PMF:**
1. Document your best customer profile in extreme detail
2. Build the playbook to replicate the reference customer, not to expand the ICP
3. Hire sales to replicate, not to expand
4. Instrument everything — you need to know what's driving retention for every new cohort
5. Don't launch new features. Remove friction from the path that's already working.
**The expansion question:** Only expand ICP when:
- You can replicate the reference customer at 3x volume with same retention
- CAC is declining (word of mouth in the reference segment)
- You've exhausted density in the reference segment
**Don't expand ICP to save the business.** Expanding ICP when retention is declining is panic, not strategy.
---
## How to Know When PMF Is Slipping
PMF is not a binary state. It can degrade. Watch for:
| Signal | What's Happening | Response |
|--------|-----------------|----------|
| D30 retention declining across cohorts | Product changes or market change are eroding value | Run Sean Ellis test immediately. Interview churned users. |
| Sean Ellis score dropping | Users less passionate about the product | Feature gap opening. Competitive pressure. |
| NPS dropping for retained users | Power users seeing degraded experience | Product quality or performance issues. |
| Organic referral rate declining | Satisfied users less enthusiastic | Product becoming commoditized. Moat eroding. |
| Support tickets shifting from feature requests to bug reports | Technical debt catching up | Engineering quality investment needed. |
| Sales cycles lengthening | ICP no longer self-evident. Positioning drift. | Re-run positioning exercise. Sharpen ICP. |
**The PMF quarterly check:**
Run Sean Ellis test every quarter. Track D30 retention by cohort every month. Put both on the CPO dashboard. These are your vital signs.
---
## Quick Reference
| Test | Threshold | Frequency |
|------|-----------|-----------|
| Sean Ellis | > 40% very disappointed | Quarterly |
| D30 retention (B2B SaaS) | > 40% | Monthly (by cohort) |
| D30 retention (consumer) | > 20% | Monthly (by cohort) |
| D90 retention (B2B SaaS) | > 45% | Monthly (by cohort) |
| Organic signup % | > 20% | Monthly |
| NPS (retained users) | > 40 | Quarterly |
| DAU/MAU (if daily product) | > 20% | Weekly |
Use `scripts/pmf_scorer.py` to run all dimensions together with weighted scoring.
FILE:references/product_org_design.md
# Product Org Design Reference
How to structure, hire, and run product organizations at different stages. No generic advice — stage-specific, role-specific, and honest about what breaks.
---
## 1. Team Topologies for Product Orgs
Matthew Skelton and Manuel Pais defined four team types. Here's how they map to product organizations.
### Four Team Types
#### Stream-Aligned Teams
Own a continuous flow of customer-facing work. They take problems all the way from discovery to delivery to measurement.
**Product org equivalent:** Feature teams, growth teams, customer journey teams.
**Characteristics:**
- Long-lived (not project teams)
- Full-stack: PM + Designer + 3-7 Engineers + QA
- Can deploy independently without asking another team
- Own their backlog, their metrics, their outcomes
**Health signals:**
- Ships without waiting on other teams more than 20% of the time
- Can define their own north star and trace it to company metric
- PMs spend > 50% of time in discovery, not coordination
**Warning signs:**
- Every sprint has "dependencies" blocking progress
- Team has PMs but engineers don't know the customer problems
- Roadmap is handed to them, not co-created
#### Platform Teams
Build and maintain shared capabilities so stream-aligned teams don't reinvent them.
**Product org equivalent:** Platform product team, internal tools, shared infrastructure.
**Characteristics:**
- Serve internal customers (other teams), not end users directly
- Measure success by stream-aligned team velocity, not feature count
- Self-service is the goal — stream teams should be unblocked without filing tickets
**Health signals:**
- Stream-aligned teams can do 80% of their work without filing a ticket to platform
- Platform has a public API and documentation, not just engineers who know how it works
- Platform team metrics include "number of teams using X without assistance"
**Warning signs:**
- Platform team has a 6-week SLA for new features
- Stream teams fork the platform to avoid waiting
- Platform team's backlog is driven by platform's own ideas, not stream team pain
**The platform product manager role:**
Platform PMs are not feature PMs. They manage internal customers. Key skills:
- Developer experience empathy (they're building for engineers)
- API and infrastructure intuition (you can't PM what you don't understand)
- Saying "no" gracefully when requests are misuses of the platform
#### Enabling Teams
Temporarily help other teams upskill in a domain. Not permanent.
**Product org equivalent:** UX research team, data literacy evangelism, accessibility experts.
**Duration:** Time-boxed. 3-6 months. Then they leave and the skill stays.
**Failure mode:** Enabling teams that never leave become coordination bottlenecks.
#### Complicated Subsystem Teams
Deep expertise required. Minimal interaction.
**Product org equivalent:** ML/AI product team, compliance product, payments, internationalization engine.
**Characteristics:**
- Specialists who can't be split across stream-aligned teams
- Interact via well-defined interface, not collaboration
- Have their own PM who understands the domain deeply
---
## 2. Org Models at Each Stage
### Pre-Seed / Seed (1-20 engineers)
**Structure:** Founder/CEO or founder/CTO is the PM. Maybe one hired PM at 15+ engineers.
**Don't build:** Process, specialization, hierarchy.
**Do build:** Direct customer access, fast iteration loops, written learning from every experiment.
**PM role at this stage:**
- Not shipping features. Talking to customers.
- Not writing specs. Running experiments.
- Not managing engineers. Being managed alongside them.
**Hiring mistake:** Hiring a "process PM" who builds Jira templates before you have PMF.
---
### Series A (20-60 engineers)
**Structure:** 2-4 PMs, organized by product area or customer journey.
```
CPO / Head of Product
├── PM — Core Product (the thing customers pay for)
├── PM — Growth / Acquisition (how more customers get there)
└── PM — Platform (as soon as engineering says they need it)
```
**What you add:** One embedded designer. Analytics shared.
**First PM hire criteria:**
- Has shipped something users use, not just wrote a spec
- Comfortable with ambiguity and no process
- Will talk to customers without being asked
- Understands the technical constraints intuitively
**What breaks at Series A:**
- Verbal communication stops working. First thing to document: the roadmap, the north star, who decided what.
- Engineers start asking "why are we building this?" — good. Answer it.
- Customer requests multiply faster than capacity. You need a prioritization framework.
---
### Series B (60-150 engineers)
**Structure:** 4-8 PMs, head of product, first design hire, embedded or dedicated analytics.
```
CPO
├── Head of Product
│ ├── PM — [Team 1] (stream-aligned)
│ ├── PM — [Team 2] (stream-aligned)
│ ├── PM — [Team 3] (stream-aligned)
│ └── PM — Platform (if engineering > 40)
├── Head of Design (or Senior Designer × 2-3)
└── Analytics (shared, or 1 embedded per team)
```
**What you add at Series B:**
- Head of Product (frees CPO from backlog, runs PM team)
- First Head of Design hire (if not already)
- Dedicated growth team (PLG or acquisition)
**What breaks at Series B:**
- PMs start optimizing their own team's metrics instead of company metrics
- Design and engineering don't talk until sprint planning
- Data team is a ticket queue — PMs can't self-serve
**Fix:** OKR alignment across teams. Design in discovery, not in handoff. Analytics tool self-serve access for every PM.
---
### Series C (150-400 engineers)
**Structure:** 8-15 PMs, multiple PM leads / directors, specialized functions.
```
CPO
├── VP / Director of Product
│ ├── PM Lead — [Product Line 1]
│ │ ├── PM
│ │ └── PM
│ ├── PM Lead — [Product Line 2]
│ │ ├── PM
│ │ └── PM
│ └── PM Lead — Platform
├── Head of Design
│ ├── UX Design
│ ├── Product Design
│ └── UX Research
├── Head of Data / Analytics
│ ├── Product Analytics
│ └── Data Science
└── Head of Product Operations
```
**What you add at Series C:**
- PM leads / directors (PMs managing PMs)
- Dedicated UX research
- Head of Product Operations (roadmap tooling, PM hiring, analytics standards, product community)
- Possible Chief of Staff (Product)
**What breaks at Series C:**
- Coordination overhead becomes the primary job
- PMs become project managers managing handoffs instead of product decisions
- Consistency across teams: 5 different ways to write a spec, 5 different analytics setups
- CPO loses touch with customers
**Fix:** Product principles (written, opinionated, used in reviews). Embedded researchers. Regular CPO customer calls (monthly minimum). Product ops to solve consistency without bureaucracy.
---
## 3. PM:Engineer Ratios
### By Stage
| Stage | Engineers | PMs | Ratio | Notes |
|-------|-----------|-----|-------|-------|
| Seed | 5 | 0-1 | 1:5 | Founder PM common |
| Series A | 20-40 | 2-4 | 1:8 | First real PMs |
| Series B | 60-100 | 5-8 | 1:10 | Platform PM emerges |
| Series C | 150-250 | 12-18 | 1:12 | PM leads required |
| Growth | 300+ | 20+ | 1:12-15 | Specialization high |
### By Team Type
| Team Type | Ratio | Rationale |
|-----------|-------|-----------|
| Stream-aligned (feature) | 1:6-8 | High discovery work, many stakeholders |
| Growth / PLG | 1:8-10 | High experimentation, more autonomy per engineer |
| Platform | 1:10-15 | Lower ambiguity, more self-directed engineers |
| Complicated subsystem (ML, payments) | 1:12-20 | Technical direction from engineers, PM is translator |
**The ratio trap:** These are guidelines, not targets. A great PM in a bad org with 12 engineers accomplishes less than a great PM with 8 in a healthy org. Fix the org before optimizing the ratio.
---
## 4. When to Hire Key Roles
### Head of Design
**Not yet signal:**
- Fewer than 2 full-time designers
- Product is primarily technical (API-first, developer tool with no GUI)
- Design is consistently described as "not a blocker"
**Hire now signal:**
- Design has become a coordination problem (who reviews what? which system? what's the standard?)
- You have 3+ designers and they're inconsistent
- CPO is spending significant time on design decisions
- Customers cite UX as a blocker to adoption
**What this person does:**
- Builds and maintains the design system
- Runs UX research as a function, not one-off projects
- Hires and grows the design team
- Keeps designers from becoming pixel-pushers and keeps them in discovery
**Wrong hire:** A senior IC who can't build process and isn't excited about it.
---
### Head of Data / Analytics
**Not yet signal:**
- < 5 PMs, data team shared with engineering
- You don't have product analytics instrumentation yet (worry about that first)
- Product metrics are reviewed monthly and nobody acts on them
**Hire now signal:**
- PMs are filing tickets for basic metric questions (sign that data team is a bottleneck)
- Multiple products with different tracking setups — no common definitions
- You want to run experiments but don't have infrastructure
- Leadership is making product decisions without data (not from choice — from access)
**What this person does:**
- Defines the event taxonomy and enforces it
- Builds self-serve analytics capability for PMs
- Runs A/B testing infrastructure
- Partners with PMs on experiment design (before launch, not after)
**Wrong hire:** A pure data scientist who can't build product analytics infrastructure and doesn't want to.
---
### Head of Product Operations
**Hire when you have:**
- 8+ PMs with inconsistent processes
- CPO spending > 30% of time on internal coordination
- No standard for roadmap tools, prioritization, or PM onboarding
- Product team can't answer "what are all teams working on this quarter?" without a 2-hour meeting
**What this person does:**
- PM onboarding and development program
- Roadmap and tooling standards (Jira, Linear, Notion — pick one and enforce it)
- Data pipelines from product to leadership (weekly metrics, OKR tracking)
- PM hiring and interview process
- Voice of product org in cross-functional coordination
**What this person does NOT do:**
- Drive product strategy (that's the CPO)
- Manage PMs (that's the Head of Product or PM leads)
- Own analytics (that's Head of Data)
---
## 5. The Product Trio
Every product team should have three roles working together from day one of discovery:
```
Product Manager → What to build and why
Product Designer → How users experience it
Tech Lead / Engineer → How to build it sustainably
```
### How the Trio Actually Works
**Discovery (weeks 1-2 of any new initiative):**
- All three in user interviews together
- All three reviewing competitive products
- All three in problem framing sessions
- Output: Opportunity, not solution
**Ideation (days):**
- All three generating solutions
- Designer prototypes 2-3 options
- Engineer provides feasibility gut check on each
- PM synthesizes against strategy
- Output: Prototype for testing
**Testing (days):**
- Designer and PM run tests (engineer optional but encouraged)
- Tests with 5-8 real customers
- All three review findings together
- Output: Decision: build, iterate, or kill
**Delivery (sprints):**
- PM writes acceptance criteria (what done looks like from user perspective)
- Engineer owns implementation
- Designer owns QA for experience quality
- All three do final review before release
### Trio Anti-Patterns
| Anti-Pattern | What It Looks Like | Why It Fails |
|-------------|-------------------|--------------|
| **PM → Designer → Engineer** | Waterfall disguised as agile | Late discovery of infeasibility and poor UX |
| **Engineer-led** | Engineers propose solutions, PM and designer polish | Builds technically correct thing nobody wants |
| **PM-led dictation** | PM writes detailed spec, team executes | Team has no context, can't make good trade-offs |
| **Designer detached** | Designers design in isolation, present to engineers | Beautiful mockup that's 8x harder to build than alternative |
| **No research** | Trio invents problems and solutions in a conference room | Building for themselves |
---
## 6. Remote vs. Co-located Product Teams
The debate is mostly settled. Here's what actually matters:
### What Changes with Remote
| Activity | Co-located | Remote | Fix |
|----------|-----------|--------|-----|
| Discovery sync | Organic, hallway | Requires scheduling | Daily async standups + weekly sync |
| Whiteboarding | Easy | Friction | Figma, Miro — async-first artifacts |
| Design review | Walk over | Calendar invite | Record reviews; written decisions |
| Relationship building | Osmotic | Deliberate | Regular 1:1s, team rituals, offsites |
| Onboarding | Shadow in person | Document-heavy | Written playbooks + buddy system |
| Difficult conversations | Easier in person | Harder | Default to video, not Slack |
### The Async-First Product Team
Works well remote IF:
- Decisions are written (Notion, Confluence, not Slack threads)
- Roadmaps are accessible to everyone without a meeting
- Product reviews are recorded and linked
- Discovery artifacts are shared before the meeting, discussed in the meeting
- 1:1s are weekly and actual (not "let's skip this week")
**What doesn't survive async:**
- Ambiguous ownership
- Verbal agreements (write it down or it didn't happen)
- Teams where "PM wrote the spec" is the only documentation
### Remote Product Org Practices
**Weekly Cadence:**
```
Monday: Async kickoff — each team posts week's focus + blockers
Tuesday: Product trio sync (30 min, per team)
Wednesday: CPO / Head of Product 1:1s
Thursday: Cross-team PM sync (30 min, rotating topics)
Friday: Async retrospective notes + week summary
```
**Monthly:**
- Full product org sync (all PMs, designers, heads)
- CPO product review (each team presents one initiative)
- Metrics review (company + team level)
**Quarterly:**
- In-person or virtual offsite
- Strategy and OKR setting
- Individual growth conversations
---
## Quick Reference
| Stage | Structure | First Hire Priority |
|-------|-----------|-------------------|
| Seed | Founder PM | Generalist PM with customer instincts |
| Series A | 2-3 PMs, flat | First real PM, owns a product area |
| Series B | Head of Product, 4-8 PMs | Head of Design |
| Series C | Org layers, PM leads | Head of Data + Product Ops |
| Growth | Full specialization | Chief of Staff (Product) |
**PM:Engineer ratio target by stage:**
Seed 1:5 → Series A 1:8 → Series B 1:10 → Series C 1:12 → Growth 1:15
**Three things that fix most product org problems:**
1. Stream-aligned teams with full-stack ownership (PM + Design + Eng)
2. OKRs that cascade from company to team to individual
3. Product trio in discovery, not just delivery
FILE:references/product_strategy.md
# Product Strategy Reference
Frameworks for product vision, competitive positioning, portfolio management, and board reporting. No theory — only what CPOs actually use.
---
## 1. Vision Frameworks
### Jobs to Be Done (JTBD)
JTBD is not a feature framework. It's a way to understand *why* customers hire your product and under what circumstances.
**The core insight:** People don't want your product. They want to make progress in their lives, and they hire your product to help. When you understand the job, you understand competition differently.
#### Conducting JTBD Interviews
**Who to interview:** Recent buyers and recent churners. Not power users — they're already converted.
**The interview script (condensed):**
```
1. "Walk me through the last time you [started using / stopped using] this product."
2. "What were you doing the day before you decided?"
3. "What else did you consider?"
4. "What almost stopped you from doing it?"
5. "Now that you're using it, what does your day look like differently?"
```
**What you're extracting:**
- **Functional job:** What task are they accomplishing?
- **Emotional job:** How do they feel during and after?
- **Social job:** How are they perceived?
- **Timeline:** What triggered the switch? (the "push" from old solution + "pull" toward new one)
- **Anxieties:** What almost prevented adoption?
- **Competing solutions:** What are they comparing you to, including "do nothing"?
#### JTBD Output: The Job Story
Format better than "user story" for strategic decisions:
```
When [situation],
I want to [motivation/job],
So I can [expected outcome].
```
**Example (healthcare scheduling):**
```
When I'm trying to coordinate my parent's care from another city,
I want to see their upcoming appointments and have someone confirm changes,
So I can feel confident they won't miss critical treatments.
```
This is a different product than "schedule management software." The strategic implications — care coordination, family access, confirmation workflows — flow from the job.
#### JTBD → Product Strategy
| Job Insight | Strategic Implication |
|-------------|----------------------|
| Job is episodic (quarterly) | Engagement model must reach them before they need it |
| Job is habitual (daily) | DAU/MAU matters; build for habit formation |
| Job has high stakes | Trust and reliability > features; invest in onboarding + support |
| Job is social | Network effects possible; virality is structural, not a campaign |
| Job is delegated (done for someone else) | Two users: the buyer and the beneficiary. Design for both. |
---
### Category Design
If you're fighting for share in an existing category, you're playing defense on someone else's field.
**Category design premise:** Companies that define the category typically capture 76% of the market cap of that category. Name the category, own it.
#### The Category Design Process
**Step 1: Name the problem, not the solution.**
```
Wrong: "We make AI-powered customer support software."
Right: "The support team doesn't need more tickets. They need fewer problems."
```
**Step 2: Define the enemy.**
The enemy is the *old way* of solving the problem, not a competitor.
- Salesforce's enemy: spreadsheets and disconnected tools (not Siebel)
- Slack's enemy: email overload (not HipChat)
- Your enemy: ___________
**Step 3: Create the category name.**
It should be obvious in hindsight, not predictable in advance. Test it:
- Does it describe the problem, not the solution?
- Is it 2-3 words?
- Could a journalist use it without quoting you?
**Step 4: Missionary selling, not mercenary selling.**
Category kings educate the market before they sell to it. Content, thought leadership, community, and free tools all matter here — not as marketing tactics but as category creation.
**Step 5: Be the reference customer.**
Get the logos that define the category. The companies others look to. When others adopt, they don't want "a tool" — they want "what [Reference Customer] uses."
---
## 2. Competitive Moats
A moat is a structural advantage that compounds over time. Features are not moats. Pricing is not a moat. A moat is why, even if a competitor perfectly copies your product today, you still win.
### Moat Type 1: Network Effects
The product becomes more valuable as more users join. Two subtypes:
**Direct network effects:** Each user makes the product better for all other users (WhatsApp, Slack).
**Indirect network effects:** Each user on one side makes the product better for the other side (Uber drivers + riders, App Store developers + users).
**Data network effects:** More users → more data → better product → more users.
#### Network Effect Diagnostic
```
Question 1: Does adding user N make the product better for user N-1?
No → You don't have direct network effects
Yes → Map exactly how and how much
Question 2: Does adding user N make the product better for users on the OTHER side?
No → You don't have indirect network effects
Yes → Identify which side is the constraint (supply or demand)
Question 3: Does using the product generate data that improves the product?
No → You don't have data network effects
Yes → What is the data flywheel? Where does it compound?
```
**Building network effects intentionally:**
- Most products accidentally have weak network effects
- Design for network effects from Day 1: sharing, notifications, collaboration, integrations
- Measure network effect strength: "What % of new users were referred by existing users?"
### Moat Type 2: Switching Costs
The cost — time, money, risk — of leaving your product. The highest switching costs are:
| Switching Cost Type | Example | CPO Action |
|--------------------|---------|-----------|
| **Data lock-in** | Years of history, reports, trained models | Make data the experience, not just the storage |
| **Workflow integration** | 23 integrations, custom automations | Every integration is a switching cost. Build them. |
| **Team adoption** | Entire team trained on your tool | Multi-seat training investments pay switching cost dividends |
| **Contractual** | Annual contracts, SLAs | Long contracts are not a moat — customers resent them |
| **Process embedding** | Your product IS their process | Aim here. This is the deepest moat. |
**Warning:** Switching costs from data lock-in without value lock-in breed resentment, not loyalty. Customers who stay because they're trapped will leave the moment a migration tool appears.
### Moat Type 3: Data Advantages
Having data others can't easily get. Three subtypes:
**Proprietary data:** Data only you have access to (exclusive partnerships, sensor networks, unique user behavior at scale).
**Data scale:** Same type of data but at 10x the volume of competitors. Scale compounds model accuracy.
**Data variety:** Unique combination of data types. Not just usage data — usage + outcome data + external context.
**Testing your data moat:**
```
1. What data do we have that competitors don't?
2. At what volume does our data create a meaningfully better product?
3. Are we at that volume? If not, when?
4. Could a competitor buy or partner their way to equivalent data?
5. Is our data improving the product automatically, or only when we analyze it manually?
```
### Moat Type 4: Economies of Scale
Unit economics improve as you scale. Infrastructure costs drop per unit. Brand recognition lowers CAC. Negotiating power increases.
This is a real moat but the weakest one for product strategy — it doesn't keep faster-moving competitors from attacking while you're small.
### Moat Scorecard
Score each moat type 0-3 for your current product:
```
0 = Not present
1 = Weak / easily replicated
2 = Meaningful / takes 12-18 months to replicate
3 = Strong / structural advantage
Network effects (direct): __/3
Network effects (indirect): __/3
Network effects (data): __/3
Switching costs (data): __/3
Switching costs (workflow): __/3
Switching costs (team): __/3
Data advantages (exclusive): __/3
Data advantages (scale): __/3
Economies of scale: __/3
Total: __/27
< 9: No meaningful moat. Compete on execution speed.
9-15: Early moat. Identify and reinforce 1-2 strongest types.
16-21: Real moat. Invest to compound it.
> 21: Strong moat. Defend and expand.
```
---
## 3. Product Positioning
Positioning is not messaging. Positioning is the choice of: *Who is this for, what does it replace, and on what dimension do we win?*
### The Positioning Canvas (after April Dunford)
```
1. Competitive Alternatives
What would customers do if your product didn't exist?
(This is your real competition, not just your vendor category)
2. Unique Attributes
What capabilities do you have that alternatives lack?
(Features, but described neutrally, not as marketing)
3. Value (Outcomes)
What does each unique attribute enable for customers?
(Bridge from feature → outcome, not feature → feature)
4. Customer Who Cares
Who values those outcomes enough to pay for them?
(The customer segment for whom this value is highest)
5. Market Category
Where does the customer put you when comparing options?
(Frame the category to win, not to be fair)
6. Relevant Trends
What's changing in the world that makes this more valuable now?
(Why this moment? Urgency enabler.)
```
### Positioning Against Three Competitors
**Positioning vs. direct competitor:**
Identify one dimension where you structurally win. "Better" is not a position.
- Win on depth: more powerful in one scenario
- Win on simplicity: fewer decisions, fewer steps
- Win on integration: works with what they already use
- Win on price/value: same outcome, lower cost or risk
**Positioning vs. indirect alternative:**
The customer's current solution (spreadsheet, manual process, point solution).
- Make switching cost obvious (what are they giving up per week?)
- Make the switch simple (migration, onboarding, no data loss)
- Find the "aha moment" fast (value before they revert)
**Positioning vs. doing nothing:**
The hardest competitor. Status quo has zero switching cost.
- Quantify the cost of inaction (time, risk, revenue, competitive risk)
- Find the trigger event that makes inaction intolerable
- Show the risk is higher than the switch cost
### Positioning Failure Modes
| Failure | Description | Fix |
|---------|-------------|-----|
| **For everyone** | No segment. "Any company that needs X." | Name the best-fit customer. |
| **Feature positioning** | "The only tool with [feature X]" | Features are table stakes. Lead with outcome. |
| **Vague differentiation** | "Easier, faster, better" | Measurable, specific, or don't say it. |
| **Category misfit** | In a category where you can't win | Either own the category or name a new one |
| **Lagging positioning** | Positioned for who you were, not who you are | Reposition every 18-24 months or after major product change |
---
## 4. Portfolio Management
### Applying BCG Matrix to Product Lines
BCG matrix was designed for business units. Applied to product lines:
**Inputs:**
- Market growth rate (industry growth, not your growth)
- Relative market share (your share vs. largest competitor)
- Revenue contribution (absolute)
- Investment level (engineering + sales + marketing per product)
**Calculation:**
```
Market share ratio = Your market share / Largest competitor's market share
Growth rate = Market CAGR (next 3 years estimate)
Stars: share ratio > 1.0, growth > 10%
Cash Cows: share ratio > 1.0, growth < 10%
Question Marks: share ratio < 1.0, growth > 10%
Dogs: share ratio < 1.0, growth < 10%
```
### Portfolio Allocation Rules
**Star products:**
- Invest at or above market growth rate
- Goal: maintain share leadership as market grows
- Don't extract cash — reinvest
- Metrics: market share trend, NPS, retention, feature velocity
**Cash Cow products:**
- Minimum investment to maintain market position
- Goal: maximize free cash flow
- Resist the urge to innovate — incremental improvements only
- Metrics: gross margin, churn rate, support cost per customer
**Question Mark products:**
- Binary decision: invest to win or exit
- "Maintain" is not a strategy for question marks — you lose share every quarter you're neutral
- Set a deadline (2 quarters) and a threshold for investment decision
- Metrics: share gain rate, customer acquisition efficiency
**Dog products:**
- Decision: sell, sunset, or bundle
- Never "fix" a dog with more investment
- Timeline to sunset: 6-12 months, migration plan for existing customers
- Metrics: customer migration rate, revenue retained
### Portfolio Review Template
Run quarterly. One slide per product.
```
Product: [Name]
Current Quadrant: [Star/Cash Cow/Question Mark/Dog]
Revenue this quarter: $___
Revenue growth QoQ: ___%
Market share estimate: ___%
Investment level (% of eng capacity): ___%
Investment posture: [Invest / Maintain / Kill]
Key metric: [Name] → [Current value] → [QoQ trend]
Top risk: [One thing that could change this assessment]
Decision required: [Yes/No] | [What decision?]
```
### The Honest Portfolio Conversation
Questions CPOs avoid but boards ask:
- "Which product would we kill if we had to? What's stopping us?"
- "Are we funding dogs because the team is attached or because there's a real plan?"
- "What would our margins look like if we stopped investing in the bottom 2 products?"
- "What's the dependency between our products? Are we a platform or a bundle of unrelated tools?"
---
## 5. Board-Level Product Reporting
### What Good Looks Like
Board product updates fail in three ways:
1. Too much roadmap detail (feature list masquerading as strategy)
2. No trend context (showing a number without showing if it's getting better or worse)
3. No risks (all good news = no credibility)
### The 5-Slide Board Product Update
**Slide 1: North Star Metric**
```
Title: Product Health — [Quarter]
[Chart: North star metric over last 12 months, quarterly cohorts]
This quarter: [Value] | Prior quarter: [Value] | YoY: [Value]
Target: [Value] | Status: On track / At risk / Behind
Drivers (2-3 bullets):
• What's driving improvement: ___
• What's dragging: ___
• What we're doing about the drag: ___
```
**Slide 2: Retention and PMF**
```
Title: Product-Market Fit Evidence
[Chart: D30 retention by cohort, last 6 cohorts]
[Callout: Sean Ellis score = XX% (target: > 40%)]
PMF status: Achieved / Approaching / Not yet
Best segment: [Describe — where retention is strongest]
Weakest segment: [Describe — and what we're doing about it]
```
**Slide 3: Portfolio Status**
```
Title: Portfolio — Invest / Maintain / Kill
| Product | Quadrant | Revenue | Growth | Posture | Risk |
|---------|---------|---------|--------|---------|------|
| [A] | Star | $___ | +XX% | Invest | ___ |
| [B] | Cash Cow| $___ | +X% | Maintain| ___ |
| [C] | Dog | $___ | -X% | Kill Q3 | ___ |
Changes since last quarter: ___
Decisions needed from board: ___
```
**Slide 4: Strategic Bets**
```
Title: Bets This Half — [H1/H2]
Bet 1: [Name]
Hypothesis: If we [do X], [segment Y] will [do Z]
Evidence so far: [Data]
Confidence: [Low / Medium / High]
Decision point: [When do we know?] [What will we measure?]
Bet 2: [Name]
[Same structure]
```
**Slide 5: Top Risks**
```
Title: Product Risks — [Quarter]
Risk 1: [Name]
What it is: ___
Probability: [Low/Med/High]
Impact if realized: ___
Mitigation: ___
Risk 2: [Name]
[Same structure]
Risk 3: [Name]
[Same structure]
```
### Delivering in the Board Meeting
- Never read the slide
- Lead with the conclusion, not the data
- Prepare for "what if that assumption is wrong?" for every bet
- When something underperformed: say it, own it, explain what changed
- Never present a number you can't explain 3 levels deep
**Example of bad delivery:**
"Our north star is up 15% QoQ, which is great. We're tracking well."
**Example of good delivery:**
"North star is up 15% — ahead of plan. The majority of that is from the enterprise cohort activated in October, driven by the workflow automation feature we shipped in September. The consumer segment is flat, which is a concern. We're running three experiments this quarter to diagnose whether that's an acquisition problem or an activation problem — I'll have an answer for next quarter."
---
## Quick Reference: Framework Summary
| Need | Framework |
|------|----------|
| Why do customers use us? | Jobs to Be Done |
| How do we define our market? | Category Design |
| What's our structural advantage? | Moat Scorecard |
| How do we position? | April Dunford Positioning Canvas |
| Which products to fund? | BCG Matrix + Invest/Maintain/Kill |
| How to report to the board? | 5-Slide Board Update |
FILE:scripts/pmf_scorer.py
#!/usr/bin/env python3
"""
PMF Scorer — Multi-dimensional Product-Market Fit analysis.
Scores PMF across four dimensions:
- Retention (40%): D30 and D90 cohort retention
- Engagement (25%): DAU/MAU, session depth, key action rate
- Satisfaction(20%): Sean Ellis score, NPS
- Growth (15%): Organic signup rate, referral rate
Usage:
python pmf_scorer.py # Run with built-in sample data
python pmf_scorer.py --input data.json # Run with your data
JSON input format: see sample_data() function below.
"""
import json
import sys
import argparse
import math
from typing import Optional
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
def sample_data() -> dict:
"""
Sample input data. Replace with your own values.
All fields are optional — missing fields score 0 for that sub-metric
and a note is added to recommendations.
"""
return {
"product_name": "Acme SaaS",
"business_model": "b2b_saas", # b2b_saas | consumer | marketplace | plg
# Retention: D30 and D90 as decimals (e.g. 0.42 = 42%)
# Provide multiple cohorts if available. Most recent first.
"retention": {
"d30_cohorts": [0.38, 0.41, 0.44, 0.43], # newest → oldest
"d90_cohorts": [0.28, 0.30, 0.31],
"curve_flattening": True, # Does the curve flatten (vs. continuing to drop)?
},
# Engagement
"engagement": {
"dau_mau_ratio": 0.24, # Daily active / Monthly active (decimal)
"avg_sessions_per_week": 3.2, # Per active user
"key_action_rate": 0.55, # % of users who performed core value action in last 30d
"session_depth_score": 0.6, # 0-1: 0 = one page, 1 = full feature exploration
},
# Satisfaction
"satisfaction": {
"sean_ellis_very_disappointed": 0.38, # Fraction (e.g. 0.38 = 38%)
"sean_ellis_sample_size": 87, # Raw response count
"nps_score": 34, # -100 to 100
"nps_sample_size": 210,
},
# Growth
"growth": {
"organic_signup_pct": 0.27, # % of new signups from organic/referral/WOM
"referral_rate": 0.18, # % of active users who referred someone last 90d
"mom_growth_rate": 0.08, # Month-over-month new user growth (decimal)
},
}
# ---------------------------------------------------------------------------
# Thresholds by business model
# ---------------------------------------------------------------------------
THRESHOLDS = {
"b2b_saas": {
"d30_pmf": 0.40, "d30_strong": 0.60,
"d90_pmf": 0.25, "d90_strong": 0.45,
"dau_mau_pmf": 0.15, "dau_mau_strong": 0.35,
"sean_ellis_pmf": 0.40, "sean_ellis_strong": 0.55,
"nps_pmf": 30, "nps_strong": 50,
},
"consumer": {
"d30_pmf": 0.20, "d30_strong": 0.35,
"d90_pmf": 0.10, "d90_strong": 0.20,
"dau_mau_pmf": 0.20, "dau_mau_strong": 0.40,
"sean_ellis_pmf": 0.40, "sean_ellis_strong": 0.55,
"nps_pmf": 20, "nps_strong": 45,
},
"marketplace": {
"d30_pmf": 0.30, "d30_strong": 0.50,
"d90_pmf": 0.20, "d90_strong": 0.35,
"dau_mau_pmf": 0.15, "dau_mau_strong": 0.30,
"sean_ellis_pmf": 0.40, "sean_ellis_strong": 0.55,
"nps_pmf": 25, "nps_strong": 45,
},
"plg": {
"d30_pmf": 0.25, "d30_strong": 0.45,
"d90_pmf": 0.15, "d90_strong": 0.30,
"dau_mau_pmf": 0.20, "dau_mau_strong": 0.40,
"sean_ellis_pmf": 0.40, "sean_ellis_strong": 0.55,
"nps_pmf": 30, "nps_strong": 50,
},
}
# Weights for the four dimensions (must sum to 1.0)
DIMENSION_WEIGHTS = {
"retention": 0.40,
"engagement": 0.25,
"satisfaction": 0.20,
"growth": 0.15,
}
# ---------------------------------------------------------------------------
# Scoring helpers
# ---------------------------------------------------------------------------
def clamp(value: float, lo: float = 0.0, hi: float = 1.0) -> float:
return max(lo, min(hi, value))
def score_between(value: Optional[float], lo: float, hi: float) -> float:
"""Linear interpolation: lo → 0.0, hi → 1.0, beyond hi → 1.0."""
if value is None:
return 0.0
if value <= lo:
return 0.0
if value >= hi:
return 1.0
return (value - lo) / (hi - lo)
def cohort_trend(cohorts: list) -> float:
"""
Given cohorts newest-first, return a trend score -1 to +1.
Positive = improving. Negative = degrading.
"""
if len(cohorts) < 2:
return 0.0
# Simple: compare most recent half average vs. older half average
mid = len(cohorts) // 2
recent_avg = sum(cohorts[:mid]) / mid if mid else cohorts[0]
older_avg = sum(cohorts[mid:]) / (len(cohorts) - mid)
if older_avg == 0:
return 0.0
delta = (recent_avg - older_avg) / older_avg
return clamp(delta * 5, -1.0, 1.0) # scale: 20% improvement = score of 1.0
# ---------------------------------------------------------------------------
# Dimension scorers
# ---------------------------------------------------------------------------
def score_retention(data: dict, thresholds: dict) -> tuple[float, list]:
"""Returns (score 0-1, list of findings)."""
r = data.get("retention", {})
findings = []
scores = []
d30 = r.get("d30_cohorts", [])
d90 = r.get("d90_cohorts", [])
if not d30:
findings.append("⚠ No D30 retention data — this is the most important PMF signal. Instrument it immediately.")
return 0.0, findings
latest_d30 = d30[0]
d30_score = score_between(latest_d30, 0, thresholds["d30_strong"])
scores.append(d30_score)
if latest_d30 >= thresholds["d30_strong"]:
findings.append(f"✓ D30 retention {latest_d30:.0%} — strong PMF signal")
elif latest_d30 >= thresholds["d30_pmf"]:
findings.append(f"◑ D30 retention {latest_d30:.0%} — approaching PMF threshold ({thresholds['d30_pmf']:.0%})")
else:
findings.append(f"✗ D30 retention {latest_d30:.0%} — below PMF threshold ({thresholds['d30_pmf']:.0%}). Focus here before anything else.")
# Trend bonus
if len(d30) >= 2:
trend = cohort_trend(d30)
trend_score = (trend + 1) / 2 # normalize to 0-1
scores.append(trend_score * 0.5) # trend is bonus, not primary
if trend > 0.1:
findings.append(f"✓ D30 retention improving across cohorts — strong learning signal")
elif trend < -0.1:
findings.append(f"✗ D30 retention declining across cohorts — product changes may be hurting core users")
if d90:
latest_d90 = d90[0]
d90_score = score_between(latest_d90, 0, thresholds["d90_strong"])
scores.append(d90_score)
if latest_d90 >= thresholds["d90_strong"]:
findings.append(f"✓ D90 retention {latest_d90:.0%} — excellent long-term retention")
elif latest_d90 >= thresholds["d90_pmf"]:
findings.append(f"◑ D90 retention {latest_d90:.0%} — some long-term value demonstrated")
else:
findings.append(f"✗ D90 retention {latest_d90:.0%} — users not finding long-term value")
else:
findings.append("⚠ No D90 data. Add 90-day cohort tracking.")
flattening = r.get("curve_flattening", False)
if flattening:
scores.append(0.8)
findings.append("✓ Retention curve flattening — core retained segment exists")
else:
scores.append(0.2)
findings.append("✗ Retention curve not flattening — no stable retained segment yet")
return clamp(sum(scores) / len(scores)), findings
def score_engagement(data: dict, thresholds: dict) -> tuple[float, list]:
e = data.get("engagement", {})
findings = []
scores = []
dau_mau = e.get("dau_mau_ratio")
if dau_mau is not None:
s = score_between(dau_mau, 0, thresholds["dau_mau_strong"])
scores.append(s)
if dau_mau >= thresholds["dau_mau_strong"]:
findings.append(f"✓ DAU/MAU {dau_mau:.0%} — strong daily habit")
elif dau_mau >= thresholds["dau_mau_pmf"]:
findings.append(f"◑ DAU/MAU {dau_mau:.0%} — moderate engagement")
else:
findings.append(f"✗ DAU/MAU {dau_mau:.0%} — users not building a habit. Find the daily job or accept weekly use pattern.")
else:
findings.append("⚠ No DAU/MAU data.")
sessions = e.get("avg_sessions_per_week")
if sessions is not None:
# 5+ sessions/week = strong, 2 = threshold
s = score_between(sessions, 1, 5)
scores.append(s)
if sessions >= 5:
findings.append(f"✓ {sessions:.1f} sessions/week — high engagement")
elif sessions >= 2:
findings.append(f"◑ {sessions:.1f} sessions/week — moderate")
else:
findings.append(f"✗ {sessions:.1f} sessions/week — very low. Users not returning within week.")
else:
findings.append("⚠ No session frequency data.")
kar = e.get("key_action_rate")
if kar is not None:
s = score_between(kar, 0.10, 0.70)
scores.append(s)
if kar >= 0.60:
findings.append(f"✓ Key action rate {kar:.0%} — core value well-adopted")
elif kar >= 0.30:
findings.append(f"◑ Key action rate {kar:.0%} — improve onboarding to drive this up")
else:
findings.append(f"✗ Key action rate {kar:.0%} — most users not reaching core value. This is an activation problem.")
else:
findings.append("⚠ No key action rate. Define your 'aha moment' action and track it.")
depth = e.get("session_depth_score")
if depth is not None:
scores.append(depth)
if depth >= 0.6:
findings.append(f"✓ Session depth {depth:.1f} — users exploring the product")
else:
findings.append(f"◑ Session depth {depth:.1f} — users sticking to narrow feature set")
if not scores:
return 0.0, findings
return clamp(sum(scores) / len(scores)), findings
def score_satisfaction(data: dict, thresholds: dict) -> tuple[float, list]:
s_data = data.get("satisfaction", {})
findings = []
scores = []
se_score = s_data.get("sean_ellis_very_disappointed")
se_n = s_data.get("sean_ellis_sample_size", 0)
if se_score is not None:
if se_n < 40:
findings.append(f"⚠ Sean Ellis n={se_n} — too small to be reliable. Need 40+ responses.")
scores.append(score_between(se_score, 0, thresholds["sean_ellis_strong"]) * 0.5) # half weight
else:
s = score_between(se_score, 0, thresholds["sean_ellis_strong"])
scores.append(s)
if se_score >= thresholds["sean_ellis_strong"]:
findings.append(f"✓ Sean Ellis {se_score:.0%} 'very disappointed' — strong PMF signal (n={se_n})")
elif se_score >= thresholds["sean_ellis_pmf"]:
findings.append(f"◑ Sean Ellis {se_score:.0%} — at PMF threshold. Push to > {thresholds['sean_ellis_strong']:.0%}.")
else:
findings.append(f"✗ Sean Ellis {se_score:.0%} — below {thresholds['sean_ellis_pmf']:.0%} threshold. Interview 'somewhat disappointed' group.")
else:
findings.append("⚠ No Sean Ellis data. Run a one-question survey to your active users now.")
nps = s_data.get("nps_score")
nps_n = s_data.get("nps_sample_size", 0)
if nps is not None:
if nps_n < 50:
findings.append(f"⚠ NPS n={nps_n} — sample too small. Need 50+ for reliability.")
# NPS ranges from -100 to 100; normalize to 0-1 against threshold
s = score_between(nps, -20, thresholds["nps_strong"])
scores.append(s)
if nps >= thresholds["nps_strong"]:
findings.append(f"✓ NPS {nps} — excellent. Promoters will drive organic growth.")
elif nps >= thresholds["nps_pmf"]:
findings.append(f"◑ NPS {nps} — acceptable. Focus on converting passives to promoters.")
elif nps >= 0:
findings.append(f"✗ NPS {nps} — low. More detractors than promoters is a warning sign.")
else:
findings.append(f"✗ NPS {nps} — negative. Active detractors outnumber promoters.")
else:
findings.append("⚠ No NPS data.")
if not scores:
return 0.0, findings
return clamp(sum(scores) / len(scores)), findings
def score_growth(data: dict, _thresholds: dict) -> tuple[float, list]:
g = data.get("growth", {})
findings = []
scores = []
organic_pct = g.get("organic_signup_pct")
if organic_pct is not None:
s = score_between(organic_pct, 0.05, 0.50)
scores.append(s)
if organic_pct >= 0.30:
findings.append(f"✓ {organic_pct:.0%} organic signups — word of mouth is working")
elif organic_pct >= 0.20:
findings.append(f"◑ {organic_pct:.0%} organic — moderate. Build referral loop deliberately.")
else:
findings.append(f"✗ {organic_pct:.0%} organic — almost all paid. PMF may not be strong enough to generate word of mouth.")
else:
findings.append("⚠ No organic signup tracking. Tag all signup sources now.")
referral = g.get("referral_rate")
if referral is not None:
s = score_between(referral, 0.05, 0.35)
scores.append(s)
if referral >= 0.25:
findings.append(f"✓ {referral:.0%} of active users referring — strong viral signal")
elif referral >= 0.15:
findings.append(f"◑ {referral:.0%} referral rate — building. Add referral incentive or friction removal.")
else:
findings.append(f"✗ {referral:.0%} referral rate — users not recommending. Satisfaction or network effects missing.")
else:
findings.append("⚠ No referral rate data.")
mom = g.get("mom_growth_rate")
if mom is not None:
s = score_between(mom, 0, 0.20)
scores.append(s)
if mom >= 0.15:
findings.append(f"✓ {mom:.0%} MoM growth — strong momentum")
elif mom >= 0.08:
findings.append(f"◑ {mom:.0%} MoM growth — moderate. Identify top acquisition channel and double it.")
else:
findings.append(f"✗ {mom:.0%} MoM growth — slow. Acquisition is a bottleneck.")
if not scores:
return 0.0, findings
return clamp(sum(scores) / len(scores)), findings
# ---------------------------------------------------------------------------
# Overall scoring and recommendations
# ---------------------------------------------------------------------------
def pmf_status(overall: float) -> tuple[str, str]:
"""Returns (status label, description)."""
if overall >= 0.80:
return "STRONG PMF", "Clear product-market fit. Shift focus to scaling acquisition and defending moat."
elif overall >= 0.60:
return "PMF APPROACHING", "Meaningful signals present. Identify and remove the 1-2 friction points blocking retention."
elif overall >= 0.40:
return "EARLY SIGNALS", "Weak PMF. Some users find value. Narrow your ICP and double down on what's working."
elif overall >= 0.20:
return "PRE-PMF", "No clear PMF yet. Don't scale acquisition. Focus entirely on retention experiments."
else:
return "NO SIGNAL", "No PMF signals detected. Revisit the problem hypothesis before investing further in the solution."
def top_recommendations(dim_scores: dict, data: dict) -> list[str]:
"""Prioritized recommendations based on weakest dimensions."""
recs = []
model = data.get("business_model", "b2b_saas")
ranked = sorted(dim_scores.items(), key=lambda x: x[1])
for dim, score in ranked:
if score < 0.40:
if dim == "retention":
recs.append(
"CRITICAL — Retention: Run cohort analysis by segment. Find the cohort with highest D30. "
"Interview 10 of those users. Build for them exclusively until retention flattens."
)
elif dim == "engagement":
recs.append(
"Engagement: Define your 'aha moment' — the one action that predicts long-term retention. "
"Measure time-to-aha. Remove every friction point on that path."
)
elif dim == "satisfaction":
recs.append(
"Satisfaction: Run Sean Ellis survey immediately (need n ≥ 40). "
"Interview every 'somewhat disappointed' user — the gap between 'somewhat' and 'very' is your product gap."
)
elif dim == "growth":
recs.append(
"Growth: Track signup source for every new user. If organic < 20%, "
"you may be papering over weak PMF with paid acquisition. Fix retention first."
)
if not recs:
recs.append(
"All dimensions scoring above threshold. Focus: "
"(1) Defend moat, (2) Expand ICP carefully, (3) Build referral flywheel."
)
if model == "b2b_saas":
recs.append("B2B tip: Track NRR (Net Revenue Retention). PMF in B2B requires expansion, not just retention.")
elif model == "consumer":
recs.append("Consumer tip: Find your D7 'magic moment'. The habit window is small — optimize for it.")
elif model == "plg":
recs.append("PLG tip: Define your PQL (product-qualified lead). The activation event that predicts paid conversion.")
elif model == "marketplace":
recs.append("Marketplace tip: Measure both sides separately. PMF on demand side ≠ PMF on supply side.")
return recs
# ---------------------------------------------------------------------------
# Report renderer
# ---------------------------------------------------------------------------
def render_report(data: dict, dim_scores: dict, dim_findings: dict, overall: float) -> str:
status, description = pmf_status(overall)
recs = top_recommendations(dim_scores, data)
lines = []
lines.append("=" * 60)
lines.append(f" PMF SCORER — {data.get('product_name', 'Product')}")
lines.append(f" Model: {data.get('business_model', 'unknown').upper()}")
lines.append("=" * 60)
lines.append("")
# Overall
bar_len = 40
filled = round(overall * bar_len)
bar = "█" * filled + "░" * (bar_len - filled)
lines.append(f" Overall PMF Score: {overall:.0%}")
lines.append(f" [{bar}]")
lines.append(f" Status: {status}")
lines.append(f" {description}")
lines.append("")
# Dimension breakdown
lines.append(" DIMENSION SCORES")
lines.append(" " + "-" * 50)
for dim, weight in DIMENSION_WEIGHTS.items():
score = dim_scores.get(dim, 0.0)
dim_bar_len = 20
dim_filled = round(score * dim_bar_len)
dim_bar = "█" * dim_filled + "░" * (dim_bar_len - dim_filled)
label = dim.capitalize().ljust(12)
lines.append(f" {label} [{dim_bar}] {score:.0%} (weight: {weight:.0%})")
lines.append("")
# Findings per dimension
for dim in ["retention", "engagement", "satisfaction", "growth"]:
findings = dim_findings.get(dim, [])
if findings:
lines.append(f" {dim.upper()} FINDINGS")
for f in findings:
lines.append(f" {f}")
lines.append("")
# Recommendations
lines.append(" PRIORITIZED RECOMMENDATIONS")
lines.append(" " + "-" * 50)
for i, rec in enumerate(recs, 1):
# Wrap at 70 chars
words = rec.split()
line = f" {i}. "
for word in words:
if len(line) + len(word) + 1 > 72:
lines.append(line)
line = " " + word + " "
else:
line += word + " "
lines.append(line.rstrip())
lines.append("")
lines.append("=" * 60)
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def run(data: dict) -> dict:
"""
Score PMF from input data dict.
Returns dict with overall score, dimension scores, and findings.
"""
model = data.get("business_model", "b2b_saas")
thresholds = THRESHOLDS.get(model, THRESHOLDS["b2b_saas"])
dim_scores = {}
dim_findings = {}
ret_score, ret_findings = score_retention(data, thresholds)
dim_scores["retention"] = ret_score
dim_findings["retention"] = ret_findings
eng_score, eng_findings = score_engagement(data, thresholds)
dim_scores["engagement"] = eng_score
dim_findings["engagement"] = eng_findings
sat_score, sat_findings = score_satisfaction(data, thresholds)
dim_scores["satisfaction"] = sat_score
dim_findings["satisfaction"] = sat_findings
grow_score, grow_findings = score_growth(data, thresholds)
dim_scores["growth"] = grow_score
dim_findings["growth"] = grow_findings
overall = sum(
dim_scores[dim] * weight
for dim, weight in DIMENSION_WEIGHTS.items()
)
return {
"overall": overall,
"dim_scores": dim_scores,
"dim_findings": dim_findings,
"status": pmf_status(overall)[0],
}
def main():
parser = argparse.ArgumentParser(
description="PMF Scorer — Multi-dimensional Product-Market Fit analysis",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--input", "-i",
metavar="FILE",
help="JSON file with your product data (default: built-in sample data)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output raw JSON instead of formatted report",
)
args = parser.parse_args()
if args.input:
try:
with open(args.input) as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: file not found: {args.input}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
else:
print("No input file provided — running with sample data.\n")
data = sample_data()
result = run(data)
if args.json:
output = {
"product_name": data.get("product_name"),
"business_model": data.get("business_model"),
"overall_score": round(result["overall"], 4),
"overall_pct": f"{result['overall']:.0%}",
"status": result["status"],
"dimensions": {
dim: {
"score": round(result["dim_scores"][dim], 4),
"pct": f"{result['dim_scores'][dim]:.0%}",
"weight": f"{DIMENSION_WEIGHTS[dim]:.0%}",
"findings": result["dim_findings"][dim],
}
for dim in DIMENSION_WEIGHTS
},
}
print(json.dumps(output, indent=2))
else:
print(render_report(data, result["dim_scores"], result["dim_findings"], result["overall"]))
if __name__ == "__main__":
main()
FILE:scripts/portfolio_analyzer.py
#!/usr/bin/env python3
"""
Portfolio Analyzer — Product portfolio BCG matrix classification and investment analysis.
For each product, classifies into BCG quadrant (Star, Cash Cow, Question Mark, Dog)
and generates investment recommendations (Invest / Maintain / Kill).
Usage:
python portfolio_analyzer.py # Run with built-in sample data
python portfolio_analyzer.py --input data.json # Run with your data
python portfolio_analyzer.py --json # Output raw JSON
JSON input format: see sample_data() function below.
"""
import json
import sys
import argparse
from typing import Optional
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
def sample_data() -> dict:
"""
Sample portfolio. Replace with real product data.
Fields:
name Product name
revenue_quarterly Current quarter revenue (any consistent currency)
revenue_prev_q Revenue last quarter (for QoQ calculation)
market_growth_pct Annual market growth rate (percent, e.g. 12.5 for 12.5%)
your_market_share Your estimated market share (percent, e.g. 8.0 for 8%)
largest_competitor_share Largest competitor's share (percent)
eng_capacity_pct % of total engineering capacity allocated (0-100)
d30_retention Optional D30 retention rate (decimal, e.g. 0.45)
nps Optional NPS score (-100 to 100)
notes Optional free text notes for the report
"""
return {
"company": "Acme Corp",
"total_engineering_headcount": 45,
"products": [
{
"name": "CorePlatform",
"revenue_quarterly": 480000,
"revenue_prev_q": 430000,
"market_growth_pct": 22.0,
"your_market_share": 18.0,
"largest_competitor_share": 12.0,
"eng_capacity_pct": 35,
"d30_retention": 0.61,
"nps": 52,
"notes": "Our flagship. Leading market share in fast-growing segment.",
},
{
"name": "ReportingModule",
"revenue_quarterly": 290000,
"revenue_prev_q": 285000,
"market_growth_pct": 5.0,
"your_market_share": 22.0,
"largest_competitor_share": 18.0,
"eng_capacity_pct": 25,
"d30_retention": 0.58,
"nps": 38,
"notes": "Mature product, strong margins, slow market.",
},
{
"name": "MobileApp",
"revenue_quarterly": 95000,
"revenue_prev_q": 78000,
"market_growth_pct": 35.0,
"your_market_share": 3.5,
"largest_competitor_share": 24.0,
"eng_capacity_pct": 28,
"d30_retention": 0.31,
"nps": 22,
"notes": "High growth market. We're far behind on share. Bet or exit.",
},
{
"name": "LegacyConnector",
"revenue_quarterly": 62000,
"revenue_prev_q": 68000,
"market_growth_pct": -3.0,
"your_market_share": 8.0,
"largest_competitor_share": 35.0,
"eng_capacity_pct": 12,
"d30_retention": 0.42,
"nps": 14,
"notes": "Declining market. Customers are on long-term contracts.",
},
],
}
# ---------------------------------------------------------------------------
# BCG Classification
# ---------------------------------------------------------------------------
# Growth rate threshold: markets growing faster than this are "high growth"
GROWTH_THRESHOLD_PCT = 10.0
# Market share ratio threshold: ratio > 1.0 means you lead the market
SHARE_RATIO_THRESHOLD = 1.0
def bcg_quadrant(market_growth_pct: float, share_ratio: float) -> str:
high_growth = market_growth_pct >= GROWTH_THRESHOLD_PCT
leading_share = share_ratio >= SHARE_RATIO_THRESHOLD
if high_growth and leading_share:
return "Star"
elif not high_growth and leading_share:
return "Cash Cow"
elif high_growth and not leading_share:
return "Question Mark"
else:
return "Dog"
def quadrant_emoji(quadrant: str) -> str:
return {
"Star": "⭐",
"Cash Cow": "🐄",
"Question Mark": "❓",
"Dog": "🐕",
}.get(quadrant, "?")
def investment_posture(quadrant: str, qoq_growth: float, retention: Optional[float]) -> str:
"""
Invest / Maintain / Kill recommendation with nuance.
"""
if quadrant == "Star":
return "Invest"
elif quadrant == "Cash Cow":
# If cash cow is declining fast or retention is poor, consider killing
if qoq_growth < -0.10 or (retention is not None and retention < 0.30):
return "Kill"
return "Maintain"
elif quadrant == "Question Mark":
# Fast QoQ growth signals the bet might pay off → Invest
# Flat or slow QoQ with weak retention → Kill
if qoq_growth >= 0.15 and (retention is None or retention >= 0.25):
return "Invest"
elif qoq_growth < 0.05 or (retention is not None and retention < 0.20):
return "Kill"
return "Evaluate" # Needs explicit strategic decision
else: # Dog
if qoq_growth > 0.10 and (retention is None or retention >= 0.35):
return "Evaluate" # Surprising momentum — verify before killing
return "Kill"
def posture_color(posture: str) -> str:
return {
"Invest": "✓",
"Maintain": "◑",
"Kill": "✗",
"Evaluate": "⚠",
}.get(posture, "?")
# ---------------------------------------------------------------------------
# Product analysis
# ---------------------------------------------------------------------------
def analyze_product(p: dict) -> dict:
revenue_q = p.get("revenue_quarterly", 0)
revenue_prev = p.get("revenue_prev_q", revenue_q)
qoq_growth = (revenue_q - revenue_prev) / revenue_prev if revenue_prev else 0.0
your_share = p.get("your_market_share", 0)
competitor_share = p.get("largest_competitor_share", 1)
share_ratio = your_share / competitor_share if competitor_share else 0.0
market_growth = p.get("market_growth_pct", 0)
retention = p.get("d30_retention")
nps = p.get("nps")
eng_pct = p.get("eng_capacity_pct", 0)
quadrant = bcg_quadrant(market_growth, share_ratio)
posture = investment_posture(quadrant, qoq_growth, retention)
# Alignment score: how well does engineering investment match the recommended posture?
# Invest products should have high eng allocation; Kill products should have low.
alignment_score = _compute_alignment(posture, eng_pct)
return {
"name": p.get("name", "Unknown"),
"revenue_quarterly": revenue_q,
"revenue_prev_q": revenue_prev,
"qoq_growth": qoq_growth,
"market_growth_pct": market_growth,
"your_market_share": your_share,
"largest_competitor_share": competitor_share,
"share_ratio": share_ratio,
"eng_capacity_pct": eng_pct,
"d30_retention": retention,
"nps": nps,
"quadrant": quadrant,
"posture": posture,
"alignment_score": alignment_score,
"notes": p.get("notes", ""),
"findings": _product_findings(quadrant, posture, qoq_growth, share_ratio,
market_growth, retention, nps, eng_pct),
}
def _compute_alignment(posture: str, eng_pct: float) -> float:
"""
Returns 0.0-1.0 score. High = engineering allocation matches strategic posture.
"""
targets = {"Invest": 0.35, "Maintain": 0.15, "Kill": 0.05, "Evaluate": 0.20}
target = targets.get(posture, 0.20)
deviation = abs(eng_pct / 100 - target)
return max(0.0, 1.0 - (deviation / 0.35))
def _product_findings(
quadrant: str, posture: str,
qoq_growth: float, share_ratio: float, market_growth: float,
retention: Optional[float], nps: Optional[int], eng_pct: float
) -> list:
findings = []
if quadrant == "Star":
if eng_pct < 30:
findings.append(f"⚠ Star product getting only {eng_pct}% of eng capacity — likely underinvested. Stars need fuel.")
else:
findings.append(f"✓ Star product with {eng_pct}% eng allocation — appropriate investment.")
if share_ratio < 1.5:
findings.append(f"◑ Share ratio {share_ratio:.1f}x — leading but not dominant. Accelerate to widen the gap.")
else:
findings.append(f"✓ Share ratio {share_ratio:.1f}x — strong lead. Defend aggressively.")
elif quadrant == "Cash Cow":
if eng_pct > 25:
findings.append(f"⚠ Cash Cow getting {eng_pct}% of eng — overinvested. Reduce to 10-15% max. Redeploy to Stars.")
else:
findings.append(f"✓ Cash Cow with {eng_pct}% eng — appropriate. Don't innovate, just maintain.")
if qoq_growth < -0.05:
findings.append(f"⚠ Revenue declining {abs(qoq_growth):.0%} QoQ — monitor for transition to Dog.")
else:
findings.append(f"✓ Revenue stable (QoQ: {qoq_growth:+.0%}) — milk this.")
elif quadrant == "Question Mark":
findings.append(f"⚠ Fast market ({market_growth:.0f}% growth) but only {share_ratio:.1f}x relative share.")
findings.append(f" Decision required: Invest to capture share or exit. 'Maintain' loses share every quarter.")
if qoq_growth >= 0.15:
findings.append(f"✓ QoQ growth {qoq_growth:+.0%} — momentum building. Investment may be justified.")
elif qoq_growth < 0.05:
findings.append(f"✗ QoQ growth {qoq_growth:+.0%} — stalled despite hot market. Strong exit signal.")
elif quadrant == "Dog":
findings.append(f"✗ Low share ({share_ratio:.1f}x) in slow/declining market ({market_growth:.0f}% growth).")
if eng_pct > 10:
findings.append(f"✗ Dog consuming {eng_pct}% of eng capacity. Set a sunset date. Migrate customers.")
if qoq_growth > 0:
findings.append(f"◑ Slight QoQ growth ({qoq_growth:+.0%}) — verify whether this is genuine or contract timing.")
if retention is not None:
if retention < 0.30:
findings.append(f"✗ D30 retention {retention:.0%} — users not finding value. Weak unit economics for any posture.")
elif retention >= 0.50:
findings.append(f"✓ D30 retention {retention:.0%} — users find value. Supports investment or stable maintenance.")
if nps is not None:
if nps < 0:
findings.append(f"✗ NPS {nps} — net detractors. Word of mouth is negative. Fix before scaling.")
elif nps >= 40:
findings.append(f"✓ NPS {nps} — strong promoter base. Harness for referrals.")
return findings
# ---------------------------------------------------------------------------
# Portfolio-level analysis
# ---------------------------------------------------------------------------
def analyze_portfolio(data: dict) -> dict:
products = [analyze_product(p) for p in data.get("products", [])]
total_revenue = sum(p["revenue_quarterly"] for p in products)
total_eng = sum(p["eng_capacity_pct"] for p in products)
# Revenue by quadrant
quadrant_revenue = {}
quadrant_eng = {}
for p in products:
q = p["quadrant"]
quadrant_revenue[q] = quadrant_revenue.get(q, 0) + p["revenue_quarterly"]
quadrant_eng[q] = quadrant_eng.get(q, 0) + p["eng_capacity_pct"]
# Portfolio health score
health = _portfolio_health(products, total_revenue, total_eng)
# Portfolio-level findings
portfolio_findings = _portfolio_findings(products, total_revenue, quadrant_revenue, quadrant_eng)
return {
"company": data.get("company", "Unknown"),
"total_engineering_headcount": data.get("total_engineering_headcount"),
"products": products,
"total_revenue_quarterly": total_revenue,
"quadrant_summary": {
q: {
"count": sum(1 for p in products if p["quadrant"] == q),
"revenue": quadrant_revenue.get(q, 0),
"revenue_pct": quadrant_revenue.get(q, 0) / total_revenue if total_revenue else 0,
"eng_pct": quadrant_eng.get(q, 0),
}
for q in ["Star", "Cash Cow", "Question Mark", "Dog"]
},
"portfolio_health_score": health,
"portfolio_findings": portfolio_findings,
}
def _portfolio_health(products: list, total_revenue: float, total_eng: float) -> float:
"""
Portfolio health 0-1. Penalizes:
- No Stars (no growth engine)
- Dogs consuming > 20% of eng
- Poor alignment scores
- Revenue concentrated in Dogs/Question Marks
"""
score = 1.0
quadrants = [p["quadrant"] for p in products]
has_star = "Star" in quadrants
has_cash_cow = "Cash Cow" in quadrants
if not has_star:
score -= 0.25 # No growth engine is a serious problem
if not has_cash_cow:
score -= 0.10 # No cash generator means funding stars from burn
# Dog eng allocation penalty
dog_eng = sum(p["eng_capacity_pct"] for p in products if p["quadrant"] == "Dog")
if dog_eng > 20:
score -= 0.20
elif dog_eng > 10:
score -= 0.10
# Revenue in dogs penalty
if total_revenue > 0:
dog_rev_pct = sum(p["revenue_quarterly"] for p in products if p["quadrant"] == "Dog") / total_revenue
if dog_rev_pct > 0.30:
score -= 0.15
# Average alignment score
avg_alignment = sum(p["alignment_score"] for p in products) / len(products) if products else 0
score -= (1 - avg_alignment) * 0.20
return max(0.0, min(1.0, score))
def _portfolio_findings(
products: list, total_revenue: float,
quadrant_revenue: dict, quadrant_eng: dict
) -> list:
findings = []
stars = [p for p in products if p["quadrant"] == "Star"]
cows = [p for p in products if p["quadrant"] == "Cash Cow"]
questions = [p for p in products if p["quadrant"] == "Question Mark"]
dogs = [p for p in products if p["quadrant"] == "Dog"]
if not stars:
findings.append("✗ CRITICAL: No Star products. You have no growth engine. Identify a Question Mark to invest in or revisit your market positioning.")
elif len(stars) == 1:
findings.append(f"◑ Single Star ({stars[0]['name']}). Portfolio is fragile — one product drives all growth. Diversify.")
else:
findings.append(f"✓ {len(stars)} Star products — healthy growth engine.")
if not cows:
findings.append("⚠ No Cash Cow products. Stars are consuming capital without a self-funding mechanism. Watch burn rate.")
else:
cow_rev = quadrant_revenue.get("Cash Cow", 0)
cow_pct = cow_rev / total_revenue if total_revenue else 0
findings.append(f"✓ Cash Cow revenue: {cow_pct:.0%} of total — funds Star investment.")
if questions:
findings.append(f"⚠ {len(questions)} Question Mark(s): {', '.join(p['name'] for p in questions)}.")
findings.append(" Each needs a binary decision: invest to win share, or exit. Set a 2-quarter deadline.")
if dogs:
dog_eng_total = sum(p["eng_capacity_pct"] for p in dogs)
findings.append(f"✗ {len(dogs)} Dog product(s): {', '.join(p['name'] for p in dogs)} consuming {dog_eng_total}% of eng capacity.")
findings.append(f" That's {dog_eng_total}% of your engineers on declining products. Set sunset dates.")
# Alignment check
misaligned = [p for p in products if p["alignment_score"] < 0.50]
if misaligned:
findings.append(f"⚠ Engineering allocation misaligned on: {', '.join(p['name'] for p in misaligned)}.")
findings.append(" Rebalance: move capacity from Dogs/Cows to Stars.")
return findings
# ---------------------------------------------------------------------------
# Report rendering
# ---------------------------------------------------------------------------
def fmt_currency(n: float) -> str:
if n >= 1_000_000:
return f".1fM"
elif n >= 1_000:
return f".0fK"
return f".0f"
def render_report(result: dict) -> str:
lines = []
lines.append("=" * 65)
lines.append(f" PORTFOLIO ANALYZER — {result['company']}")
lines.append(f" Total Quarterly Revenue: {fmt_currency(result['total_revenue_quarterly'])}")
if result.get("total_engineering_headcount"):
lines.append(f" Engineering Headcount: {result['total_engineering_headcount']}")
lines.append("=" * 65)
lines.append("")
# Portfolio health
health = result["portfolio_health_score"]
bar_len = 40
filled = round(health * bar_len)
bar = "█" * filled + "░" * (bar_len - filled)
lines.append(f" Portfolio Health: {health:.0%}")
lines.append(f" [{bar}]")
lines.append("")
# Quadrant summary
lines.append(" QUADRANT SUMMARY")
lines.append(" " + "-" * 55)
header = f" {'Quadrant':<15} {'Count':>5} {'Revenue':>10} {'Rev%':>6} {'Eng%':>6}"
lines.append(header)
lines.append(" " + "-" * 55)
total_rev = result["total_revenue_quarterly"]
for q in ["Star", "Cash Cow", "Question Mark", "Dog"]:
qs = result["quadrant_summary"][q]
emoji = quadrant_emoji(q)
label = f"{emoji} {q}"
rev_pct = f"{qs['revenue_pct']:.0%}" if qs["count"] else "-"
eng = f"{qs['eng_pct']}%" if qs["count"] else "-"
rev = fmt_currency(qs["revenue"]) if qs["count"] else "-"
lines.append(f" {label:<15} {qs['count']:>5} {rev:>10} {rev_pct:>6} {eng:>6}")
lines.append("")
# Per-product breakdown
lines.append(" PRODUCT BREAKDOWN")
lines.append(" " + "-" * 65)
for p in result["products"]:
emoji = quadrant_emoji(p["quadrant"])
pc = posture_color(p["posture"])
lines.append(
f" {emoji} {p['name']} — {p['quadrant']} → {pc} {p['posture']}"
)
lines.append(
f" Revenue: {fmt_currency(p['revenue_quarterly'])}/qtr "
f"QoQ: {p['qoq_growth']:+.0%} "
f"Mkt growth: {p['market_growth_pct']:+.0f}%"
)
lines.append(
f" Share ratio: {p['share_ratio']:.1f}x "
f"Eng: {p['eng_capacity_pct']}% "
f"Alignment: {p['alignment_score']:.0%}"
)
if p.get("d30_retention") is not None:
lines.append(
f" D30 retention: {p['d30_retention']:.0%} "
f"NPS: {p['nps'] if p['nps'] is not None else 'N/A'}"
)
if p.get("notes"):
lines.append(f" Note: {p['notes']}")
for f in p.get("findings", []):
lines.append(f" {f}")
lines.append("")
# Portfolio-level findings
lines.append(" PORTFOLIO FINDINGS")
lines.append(" " + "-" * 65)
for f in result.get("portfolio_findings", []):
lines.append(f" {f}")
lines.append("")
lines.append("=" * 65)
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Portfolio Analyzer — BCG matrix classification and investment recommendations",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--input", "-i",
metavar="FILE",
help="JSON file with portfolio data (default: built-in sample data)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output raw JSON result",
)
args = parser.parse_args()
if args.input:
try:
with open(args.input) as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: file not found: {args.input}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
else:
print("No input file provided — running with sample data.\n")
data = sample_data()
result = analyze_portfolio(data)
if args.json:
# Make result JSON-serializable
def clean(obj):
if isinstance(obj, dict):
return {k: clean(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [clean(v) for v in obj]
elif isinstance(obj, float):
return round(obj, 4)
return obj
print(json.dumps(clean(result), indent=2))
else:
print(render_report(result))
if __name__ == "__main__":
main()
Operations leadership for scaling companies. Process design, OKR execution, operational cadence, and scaling playbooks. Use when designing operations, settin...
---
name: "coo-advisor"
description: "Operations leadership for scaling companies. Process design, OKR execution, operational cadence, and scaling playbooks. Use when designing operations, setting up OKRs, building processes, scaling teams, analyzing bottlenecks, planning operational cadence, or when user mentions COO, operations, process improvement, OKRs, scaling, operational efficiency, or execution."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: coo-leadership
updated: 2026-03-05
python-tools: ops_efficiency_analyzer.py, okr_tracker.py
frameworks: scaling-playbook, ops-cadence, process-frameworks
---
# COO Advisor
Operational frameworks and tools for turning strategy into execution, scaling processes, and building the organizational engine.
## Keywords
COO, chief operating officer, operations, operational excellence, process improvement, OKRs, objectives and key results, scaling, operational efficiency, execution, bottleneck analysis, process design, operational cadence, meeting cadence, org scaling, lean operations, continuous improvement
## Quick Start
```bash
python scripts/ops_efficiency_analyzer.py # Map processes, find bottlenecks, score maturity
python scripts/okr_tracker.py # Cascade OKRs, track progress, flag at-risk items
```
## Core Responsibilities
### 1. Strategy Execution
The CEO sets direction. The COO makes it happen. Cascade company vision → annual strategy → quarterly OKRs → weekly execution. See `references/ops_cadence.md` for full OKR cascade framework.
### 2. Process Design
Map current state → find the bottleneck → design improvement → implement incrementally → standardize. See `references/process_frameworks.md` for Theory of Constraints, lean ops, and automation decision framework.
**Process Maturity Scale:**
| Level | Name | Signal |
|-------|------|--------|
| 1 | Ad hoc | Different every time |
| 2 | Defined | Written but not followed |
| 3 | Measured | KPIs tracked |
| 4 | Managed | Data-driven improvement |
| 5 | Optimized | Continuous improvement loops |
### 3. Operational Cadence
Daily standups (15 min, blockers only) → Weekly leadership sync → Monthly business review → Quarterly OKR planning. See `references/ops_cadence.md` for full templates.
### 4. Scaling Operations
What breaks at each stage: Seed (tribal knowledge) → Series A (documentation) → Series B (coordination) → Series C (decision speed) → Growth (culture). See `references/scaling_playbook.md` for detailed playbook per stage.
### 5. Cross-Functional Coordination
RACI for key decisions. Escalation framework: Team lead → Dept head → COO → CEO based on impact scope.
## Key Questions a COO Asks
- "What's the bottleneck? Not what's annoying — what limits throughput."
- "How many manual steps? Which break at 3x volume?"
- "Who's the single point of failure?"
- "Can every team articulate how their work connects to company goals?"
- "The same blocker appeared 3 weeks in a row. Why isn't it fixed?"
## Operational Metrics
| Category | Metric | Target |
|----------|--------|--------|
| Execution | OKR progress (% on track) | > 70% |
| Execution | Quarterly goals hit rate | > 80% |
| Speed | Decision cycle time | < 48 hours |
| Quality | Customer-facing incidents | < 2/month |
| Efficiency | Revenue per employee | Track trend |
| Efficiency | Burn multiple | < 2x |
| People | Regrettable attrition | < 10% |
## Red Flags
- OKRs consistently 1.0 (not ambitious) or < 0.3 (disconnected from reality)
- Teams can't explain how their work maps to company goals
- Leadership meetings produce no action items two weeks running
- Same blocker in three consecutive syncs
- Process exists but nobody follows it
- Departments optimize local metrics at expense of company metrics
## Integration with Other C-Suite Roles
| When... | COO works with... | To... |
|---------|-------------------|-------|
| Strategy shifts | CEO | Translate direction into ops plan |
| Roadmap changes | CPO + CTO | Assess operational impact |
| Revenue targets change | CRO | Adjust capacity planning |
| Budget constraints | CFO | Find efficiency gains |
| Hiring plans | CHRO | Align headcount with ops needs |
| Security incidents | CISO | Coordinate response |
## Detailed References
- `references/scaling_playbook.md` — what changes at each growth stage
- `references/ops_cadence.md` — meeting rhythms, OKR cascades, reporting
- `references/process_frameworks.md` — lean ops, TOC, automation decisions
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- Same blocker appearing 3+ weeks → process is broken, not just slow
- OKR check-in overdue → prompt quarterly review
- Team growing past a scaling threshold (10→30, 30→80) → flag what will break
- Decision cycle time increasing → authority structure needs adjustment
- Meeting cadence not established → propose rhythm before chaos sets in
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Set up OKRs" | Cascaded OKR framework (company → dept → team) |
| "We're scaling fast" | Scaling readiness report with what breaks next |
| "Our process is broken" | Process map with bottleneck identified + fix plan |
| "How efficient are we?" | Ops efficiency scorecard with maturity ratings |
| "Design our meeting cadence" | Full cadence template (daily → quarterly) |
## Reasoning Technique: Step by Step
Map processes sequentially. Identify each step, handoff, and decision point. Find the bottleneck using throughput analysis. Propose improvements one step at a time.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:references/ops_cadence.md
# Operational Cadence: Meetings, Async, Decisions, and Reporting
> The rhythm of your company determines its output. Bad cadence = constant context-switching, decisions made without information, and a leadership team that's always reactive.
---
## Philosophy
**Meetings are a tax.** Every hour in a meeting is an hour not spent building, selling, or serving customers. A good cadence minimizes meeting time while ensuring the right people have the right information at the right time.
**Async is default, sync is exception.** Most information sharing and routine updates should happen in writing. Reserve synchronous time for things that genuinely require real-time discussion: decisions with significant disagreement, complex problem-solving, relationship-building.
**Cadence serves strategy.** The calendar reflects priorities. If you're doing monthly all-hands but weekly status updates, you've inverted the importance.
---
## Meeting Cadence Templates
### Daily Operations
#### Daily Standup (Engineering / Product Teams)
**Format:** Async-first (Slack/Loom); sync only if blocked
**Sync duration:** 15 minutes max
**Participants:** Team (5–10 people)
**Facilitator:** Team lead or rotating
```
ASYNC FORMAT (post in #standup channel):
Yesterday: [What I completed]
Today: [What I'm working on]
Blocked: [Anything blocking me — tag the person who can unblock]
```
**Rules:**
- No status reporting in sync standup if everyone can read the async update
- Standups are not problem-solving sessions — take issues offline
- Skip standup if the team has a full-team session that day
- Kill standup if the team consistently has nothing blocked; replace with async
#### Daily Leadership Check-in (COO)
**Format:** Async only — read, don't meet
**Time:** 8:00–8:30 AM
**COO morning read:**
1. Yesterday's key metrics dashboard (5 min)
2. Overnight Slack/email escalations (5 min)
3. Today's decisions needed list (5 min)
4. Any P0/P1 incidents (check status page + on-call logs)
---
### Weekly Cadence
#### Leadership Sync (Weekly)
**Duration:** 60–90 minutes
**Participants:** C-suite + VP level
**Owner:** COO (or CEO)
**Day/Time:** Monday or Tuesday, morning
```
AGENDA TEMPLATE:
00:00–10:00 Metrics pulse (pre-read required — no presenting charts)
- Revenue: ACV, pipeline, churn delta
- Product: shipped last week, blockers this week
- Engineering: incidents, velocity
- CS: escalations, NPS delta
- People: open reqs, attrition flag
10:00–45:00 Priority items (submitted in advance, max 3)
- Item 1: [Owner: Name] [Decision needed / FYI / Input needed]
- Item 2: [Owner: Name]
- Item 3: [Owner: Name]
45:00–60:00 Parking lot / open
- Anything not covered
- Next week flagging
```
**Pre-meeting requirements:**
- Metrics dashboard updated by EOD Friday
- Priority items submitted by Sunday 6 PM
- Anyone who hasn't read the pre-read gets no floor time
**Output:** Decision log updated with outcomes, action items assigned in tracking system
#### 1:1 (Manager ↔ Direct Report)
**Duration:** 30–45 minutes
**Frequency:** Weekly (skip-levels: bi-weekly)
**Owner:** Report (the direct report sets agenda)
```
1:1 STRUCTURE:
[5 min] What's on your mind / temperature check
[15 min] Their agenda — what they want to discuss
[10 min] Manager agenda — feedback, context, decisions
[5 min] Action items review from last week
```
**1:1 anti-patterns to eliminate:**
- Using 1:1 for status updates (that's what standups are for)
- Manager dominating the agenda
- Skipping because "things are fine"
- No written record of what was discussed
**Private 1:1 doc:** Every manager/report pair maintains a shared doc with running notes, action items, and career development thread.
#### Cross-Functional Weekly Sync
**Duration:** 45 minutes
**Participants:** 2–4 team leads with shared dependencies
**Examples:** Product + Engineering, Sales + CS, Marketing + Sales
```
AGENDA:
00–10 Shared metrics (things both teams care about)
10–30 Active collaboration items — what needs coordination this week
30–40 Blockers + dependencies (what do I need from your team?)
40–45 Upcoming: what's coming that the other team should know about
```
---
### Monthly Cadence
#### All-Hands / Town Hall
**Duration:** 60–90 minutes
**Participants:** Entire company
**Owner:** CEO + functional heads
**Format:** In-person preferred; video if distributed
```
ALL-HANDS AGENDA (60 min version):
00–05 Opening — CEO sets the tone
05–20 Business update
- Where we are vs. plan (actuals vs. budget)
- Key wins and learning moments from last month
- What we're focused on this month
20–40 Functional spotlights (2 functions, 10 min each)
- What we shipped / what we did
- What we learned
- What's next
40–55 Open Q&A (no screened questions — take everything)
55–60 Closing
ALL-HANDS PREP CHECKLIST:
□ CEO talking points reviewed 48h in advance
□ Metrics slides reviewed by Finance for accuracy
□ Q&A prep — leadership team briefs on likely questions
□ Recording setup confirmed
□ Async option for timezones (recording posted within 2h)
□ Action items from Q&A captured and published within 24h
```
#### Monthly Business Review (MBR)
**Duration:** 2 hours
**Participants:** Leadership team
**Owner:** COO
```
MBR AGENDA:
00–20 Financial review (Finance presents)
- Revenue vs. plan, by segment
- Burn rate, runway
- Headcount actual vs. plan
- Key cost drivers
20–60 Functional reviews (each VP, 8 min each)
Standard template per function:
- Metrics: [3 key metrics vs. prior month vs. plan]
- Wins: [top 2-3 wins]
- Gaps: [where we missed and why]
- Next 30 days: [top 3 priorities]
60–90 Strategic topics (pre-submitted)
- Items requiring cross-functional decision
- Risks or issues needing leadership visibility
90–110 Decisions and action items
- Document decisions made
- Assign owners and deadlines
110–120 Retrospective
- What's working in how we operate?
- What needs to change?
```
**MBR pre-read package** (published 48h before):
- Financial summary (1 page)
- Each function's 1-pager (see template below)
```
FUNCTIONAL 1-PAGER TEMPLATE:
Function: [Name] Month: [Month Year]
Owner: [VP Name]
TOP METRICS:
| Metric | Target | Actual | vs. LM | vs. Plan |
|--------|--------|--------|--------|----------|
| [M1] | | | | |
| [M2] | | | | |
| [M3] | | | | |
WINS (2-3 bullets):
•
•
GAPS (be honest — no spin):
•
•
DEPENDENCIES (what I need from other teams):
•
NEXT 30 DAYS (top 3 priorities):
1.
2.
3.
```
---
### Quarterly Cadence
#### Quarterly Business Review (QBR)
**Duration:** Half day (4 hours)
**Participants:** Leadership team + key functional leads
**Owner:** CEO + COO
```
QBR AGENDA (4 hours):
PART 1: Look back (90 min)
- CEO: Business context and narrative (15 min)
- Finance: Full quarter P&L review (20 min)
- Each function: 10-min review against OKRs
Format: Hit/Miss/Partial for each objective + root cause
PART 2: Look forward (90 min)
- Product/Engineering: What ships next quarter (20 min)
- Sales/Marketing: Pipeline and demand plan (20 min)
- People: Headcount plan and key hires (15 min)
- Finance: Budget and forecast (20 min)
- Cross-functional dependencies (15 min)
PART 3: Strategic discussion (60 min)
- 1–2 strategic topics requiring deep discussion
- Pre-submitted and pre-read
PART 4: OKR setting for next quarter (30 min)
- Draft OKRs reviewed and challenged
- Final OKRs locked or assigned for next week finalization
```
#### Quarterly Leadership Off-site
**Duration:** 1–2 days (Series B+)
**Participants:** C-suite + VPs
**Purpose:** Strategy alignment, relationship building, hard conversations
**Off-site agenda principles:**
- No laptops during sessions (phones away)
- At least 50% discussion, max 50% presentation
- Include one session on how the leadership team is functioning (not just what the business is doing)
- Output: 1-page summary of decisions and commitments shared with the company
---
### Annual Cadence
#### Annual Planning Cycle
**Timeline:** Start 8–10 weeks before fiscal year end
```
ANNUAL PLANNING TIMELINE:
Week -10: Company strategic priorities draft (CEO + COO)
Week -8: Revenue model + market analysis (Finance + Sales)
Week -7: Functional goal-setting begins
Week -6: Headcount planning by function
Week -5: Draft plans reviewed by COO
Week -4: Cross-functional dependency alignment
Week -3: Budget finalization
Week -2: Board review (if applicable)
Week -1: Final company OKRs published
Week 0: Year kick-off all-hands
```
#### Year Kick-off All-Hands
**Duration:** 2–4 hours
**Participants:** Entire company
**Purpose:** Align entire company on year strategy and goals
```
KICK-OFF AGENDA:
- Last year retrospective: What we accomplished, what we learned
- Market context: Why now, why us
- Year strategy: The 2-3 things that matter most
- OKRs: Company-level goals, each function's goals
- Culture: How we'll work together
- Q&A: Open and honest
```
---
## Async Communication Frameworks
### The Writing-First Culture
All communication defaults to written unless real-time is genuinely necessary. This is how you scale decision-making without scaling meetings.
**Written first means:**
- Decisions are documented before they're communicated
- Updates are published before questions are asked
- Problems are described before solutions are proposed
### Slack Channel Architecture
```
REQUIRED CHANNELS:
#announcements Read-only. Major company announcements only.
#general Company-wide conversation
#leadership-public Leadership decisions visible to all (transparency)
#incidents P0/P1 incidents only. Auto-resolved when incident is closed.
#metrics Automated metric updates. No discussion here.
#wins Customer wins, team wins. Culture channel.
FUNCTIONAL CHANNELS:
#engineering, #product, #sales, #marketing, #cs, #people, #finance
PROJECT CHANNELS:
#proj-[name] Temporary. Archive when project ships.
DECISION CHANNELS:
#decisions All cross-team decisions logged here with context
```
**Anti-patterns to eliminate:**
- DMs for work decisions (decisions belong in channels, visible to team)
- @channel abuse (train people — this means everyone stops what they're doing)
- Thread avoidance (all replies go in threads, period)
- Multiple channels for same function (merge aggressively)
### Async Decision Template
When a decision needs input but doesn't require a meeting:
```
DECISION REQUEST (post in #decisions):
**Context:** [1-3 sentences on why this decision is needed]
**Options considered:**
A) [Option A] — Pros: X. Cons: Y.
B) [Option B] — Pros: X. Cons: Y.
**Recommendation:** [Your recommendation and why]
**Input needed from:** @person1, @person2 (tag specific people)
**Decide by:** [Date/Time — give at least 24 hours]
**If no response:** [Default action if no input received]
```
### Loom / Video for Async Communication
Use async video for:
- Explaining complex technical architecture
- Walking through a design or document with context
- Giving feedback that needs tone/nuance
- Team updates that would otherwise be a meeting
**Loom best practices:**
- Keep under 5 minutes; break up anything longer
- Always include a summary comment with key points
- Ask viewers to leave timestamp comments for specific questions
---
## Decision-Making Frameworks
### RAPID
The most practical decision-making framework for startups scaling to enterprises.
| Role | Meaning | Responsibility |
|------|---------|---------------|
| **R** — Recommend | Proposes decision with analysis | Does the work, gathers input, makes recommendation |
| **A** — Agree | Must agree before decision is final | Has veto power; should be used sparingly |
| **P** — Perform | Executes the decision | Consulted during recommendation phase |
| **I** — Input | Consulted for perspective | Shares point of view; not binding |
| **D** — Decide | Makes the final call | One person only — groups don't decide |
**How to use RAPID:**
1. For every significant decision, explicitly assign R, A, P, I, D before work begins
2. The D role is always one person — never a committee
3. Agree (A) roles should be limited to 2–3 people maximum; more = paralysis
4. Post the RAPID in the decision doc so everyone knows the structure
**Example application:**
```
Decision: Migrate from PostgreSQL to distributed database
R: VP Engineering
A: CTO, COO (for cost implications)
P: Infrastructure team
I: Product leads, Finance
D: CTO
```
### RACI
Better for ongoing processes than one-time decisions. Use RACI for recurring operational responsibilities.
| Role | Meaning |
|------|---------|
| **R** — Responsible | Does the work |
| **A** — Accountable | Owns the outcome; one person only |
| **C** — Consulted | Input before decisions/actions |
| **I** — Informed | Told of decisions/actions after the fact |
**RACI matrix template:**
```
PROCESS: Customer Escalation Handling
Task | CS Lead | VP CS | Eng Lead | CEO
------------------------|---------|-------|----------|----
Receive escalation | R | I | I | -
Diagnose issue | R | C | C | -
Communicate to customer | R | A | - | I (major)
Resolve technical issue | C | - | R | -
Close escalation | R | A | I | -
Post-mortem (P0/P1) | C | A | R | I
```
**Common RACI mistakes:**
- Multiple A roles (breaks accountability)
- R and A always same person (defeats the purpose)
- Too many C roles (everyone's consulted, nothing moves)
- Not distinguishing C from I (different obligations)
### DRI (Directly Responsible Individual)
Apple's framework; used widely in fast-moving tech companies. Simpler than RAPID/RACI for internal use.
**The rule:** Every project, deliverable, and decision has exactly one DRI. The DRI is the person who gets credit when it succeeds and gets called on when it fails. No DRI = no accountability.
**DRI requirements:**
- Listed by name in every project brief
- Has authority to make decisions within scope
- Is responsible for communicating status
- Cannot blame lack of resources — their job is to escalate when blocked
**DRI vs. RACI:** Use DRI for project ownership and RACI for process ownership. They complement each other.
### Decision Log
Every significant decision gets logged. Significant = affects more than one team, costs more than $10K, or is difficult to reverse.
```
DECISION LOG FORMAT:
Date: [YYYY-MM-DD]
Decision: [One sentence summary]
Context: [Why was this decision needed? What was the situation?]
Options considered: [What alternatives were evaluated?]
Decision made: [What was decided?]
Rationale: [Why this option?]
Owner: [Who made the final call?]
Reversible: [Yes / No / Partially]
Review date: [When should this decision be revisited?]
Outcome: [Filled in later — what actually happened?]
```
---
## Reporting Templates
### Weekly CEO/COO Dashboard
```
COMPANY HEALTH — WEEK OF [DATE]
REVENUE
ARR: $[X]M (vs. plan: +/-X%, vs. LW: +/-X%)
New ARR this week: $[X]K
Churned ARR: $[X]K
Pipeline (90-day): $[X]M
PRODUCT
Shipped this week: [Brief list]
P0/P1 incidents: [Count] — [1-line summary if any]
Deploy frequency: [X per week]
CUSTOMER
Active customers: [X]
NPS (rolling 30d): [X]
Open escalations: [X] (P0: [X], P1: [X])
PEOPLE
Headcount: [X] (vs. plan: [X])
Open reqs: [X]
Attrition (30d): [X]
CASH
Cash on hand: $[X]M
Burn (last 30d): $[X]M
Runway: [X] months
🔴 ISSUES (needs leadership attention):
•
•
🟡 WATCH (monitor, no action yet):
•
🟢 WINS:
•
```
### Monthly Investor/Board Update
```
[COMPANY NAME] — MONTHLY UPDATE — [MONTH YEAR]
THE HEADLINE
[2-3 sentences: what was the defining story of this month?]
KEY METRICS
| Metric | [Month] | vs. Prior | vs. Plan |
|--------|---------|-----------|----------|
| ARR | | | |
| MRR Added | | | |
| Churn | | | |
| NRR | | | |
| Burn | | | |
| Runway | | | |
WINS
1. [Specific, concrete win with numbers]
2. [Second win]
3. [Third win]
CHALLENGES
1. [Honest description of challenge + what you're doing about it]
2. [Second challenge]
KEY DECISIONS MADE
• [Decision + brief rationale]
ASKS FROM INVESTORS
• [Specific ask with context — intros, advice, etc.]
NEXT MONTH PRIORITIES
1.
2.
3.
```
### Quarterly OKR Progress Report
```
Q[X] OKR PROGRESS — [COMPANY NAME]
SCORING GUIDE:
🟢 On track (>70% confidence of hitting target)
🟡 At risk (50-70% confidence)
🔴 Off track (<50% confidence)
COMPANY OBJECTIVES:
O1: [Objective title]
KR1.1: [Key Result] ............... [X]% 🟢
KR1.2: [Key Result] ............... [X]% 🟡
Objective confidence: 🟢 | Notes: [1 line]
O2: [Objective title]
KR2.1: [Key Result] ............... [X]% 🔴
KR2.2: [Key Result] ............... [X]% 🟢
Objective confidence: 🟡 | Notes: [1 line]
FUNCTIONAL OBJECTIVES:
[Same format per function]
OVERALL QUARTER HEALTH: 🟡
Summary: [2-3 sentences on overall trajectory]
TOP 3 ACTIONS TO GET BACK ON TRACK:
1. [Action + owner + deadline]
2.
3.
```
---
## Cadence Anti-Patterns to Eliminate
| Anti-Pattern | What It Looks Like | Fix |
|---|---|---|
| **Meeting creep** | Calendar blocks added over time, never removed | Quarterly calendar audit — delete all recurring meetings, re-add only what's essential |
| **Update theater** | Meetings where people read from slides | Require pre-reads; ban in-meeting presentations |
| **Decision avoidance** | Topics recur across multiple meetings | Assign a D (decider) before the meeting. If no D, don't hold the meeting. |
| **Sync for async** | Using meetings for information sharing | Move updates to Loom/Slack; protect sync time for discussion |
| **HIPPO problem** | Highest-paid person in room wins | Structure discussions so data is presented before opinions |
| **Retrospective theater** | Retros with no action items | Every retro must produce ≥1 committed change |
| **Silent agenda** | Agenda not shared until meeting starts | Agendas published 24h in advance, required reading |
---
*Cadence framework synthesized from Amazon's PR/FAQ culture, Google's OKR playbook, GitLab's remote work handbook, and operational patterns from 50+ Series A–C companies.*
FILE:references/process_frameworks.md
# Process Frameworks for Startup Operations
> Theory of Constraints, Lean, process mapping, automation, and change management — applied to real startup contexts, not factory floors.
---
## Part 1: Theory of Constraints (TOC) Applied to Startups
### What TOC Actually Says
Eliyahu Goldratt's core insight: **every system has exactly one constraint that limits throughput.** Improving anything other than the constraint is waste. The goal isn't to optimize every function — it's to identify the single bottleneck and exploit it until a new constraint emerges.
**The Five Focusing Steps:**
1. **Identify** the constraint — what limits the system's output?
2. **Exploit** it — get maximum output from the constraint without adding resources
3. **Subordinate** everything else — other activities serve the constraint's needs
4. **Elevate** it — add resources to increase constraint capacity
5. **Repeat** — when the constraint moves, find the new one
### Finding the Constraint in Your Startup
The constraint is almost never where people think it is. Sales thinks it's Marketing. Engineering thinks it's Product. Everyone thinks it's someone else.
**Method:** Map your value stream (see Part 3), measure throughput at each step, find the step with the lowest throughput or the highest queue in front of it.
**Common startup constraints by stage:**
| Stage | Most Common Constraint | Why |
|-------|----------------------|-----|
| Pre-PMF | Learning speed | Not enough customer feedback cycles |
| Series A | Sales capacity | Demand > sales team's ability to close |
| Series B | Engineering velocity | Product backlog growing faster than shipping rate |
| Series C | Onboarding throughput | New customer volume > CS team's onboarding capacity |
| Growth | Hiring throughput | Headcount plan > recruiting team's capacity |
### Applying TOC to Product Development
**The five visible constraints in product development:**
**1. Requirements clarity**
*Symptom:* Engineering asks for clarification mid-sprint. Tickets re-opened. Scope creep.
*Fix:* Never pull a story into sprint until acceptance criteria are written and reviewed. Product manager must be available same-day for clarification.
**2. Review and approval bottleneck**
*Symptom:* PRs sit unreviewed for >24 hours. Deploys waiting for sign-off.
*Fix:* Code review SLA: 2-hour response for small PRs (<100 lines), 4-hour for medium. Design reviews: 24-hour turnaround. Anyone waiting >SLA can escalate to manager.
**3. QA throughput**
*Symptom:* "Done" pile grows faster than QA can test. Release day crunch.
*Fix:* QA is pulled into sprint planning and sprint review. Testing starts as features finish, not all at end. Automated test coverage as a sprint exit criterion.
**4. Deployment pipeline speed**
*Symptom:* Deploy takes 45+ minutes. Engineers wait. Hotfix urgency causes dangerous shortcuts.
*Fix:* Measure deploy time weekly. Set target (10 min for most apps). Build optimization into engineering roadmap as a real ticket.
**5. Feedback loop latency**
*Symptom:* You ship features and don't know if they worked for weeks.
*Fix:* Every shipped feature has instrumented metrics reviewed within 5 business days. If no metrics exist, feature doesn't ship.
### Applying TOC to Sales
**The sales pipeline as a system of constraints:**
```
Lead generation → Qualification → Demo → Proposal → Negotiation → Close
[X] → [X] → [X] → [X] → [X] → [X]
Measure: conversion rate and time-in-stage at each step.
The constraint is the step with the LOWEST conversion rate × volume.
```
**Example diagnosis:**
- Lead → Qualified: 40% conversion, 2 days
- Qualified → Demo: 80% conversion, 5 days ← High conversion but slow (queue)
- Demo → Proposal: 60% conversion, 3 days
- Proposal → Close: 30% conversion, 14 days ← **Constraint** (lowest conversion)
*Diagnosis:* Proposals are being sent to wrong buyers or proposals aren't compelling. Fix: proposal template audit, champion coaching, economic buyer access earlier in process.
---
## Part 2: Lean Operations for Tech Companies
### The Lean Toolkit (What's Actually Useful)
Lean Manufacturing was designed for car factories. Most of the original toolkit doesn't apply to software. Here's what does:
**Value Stream Mapping** — Map the full flow of work from customer request to delivery. Label value-add time vs. wait time. Most processes are 90% wait time and 10% actual work.
**5S** — Sort, Set in order, Shine, Standardize, Sustain. Applied to digital work:
- *Sort:* Delete unused tools, channels, documents
- *Set in order:* Organize information architecture so things are findable
- *Shine:* Regular cleanup sprints (documentation, tech debt, tool hygiene)
- *Standardize:* Templates, conventions, naming standards
- *Sustain:* Assign owners; entropy is the default state
**Pull vs. Push** — Don't push work onto people's plates. Pull = people take work when they have capacity. Push = work is assigned to people regardless of capacity. Most companies push; lean companies pull.
**Kaizen** — Continuous small improvements. Build this into your operating rhythm:
- Weekly: each team identifies one small improvement to their process
- Monthly: review and close out improvement items
- Quarterly: broader process retrospective
**Waste Categories (TIMWOODS) — Applied to Operations:**
| Waste Type | Factory Example | Startup Example |
|-----------|----------------|-----------------|
| **T**ransportation | Moving parts | Handing off work between tools with no integration |
| **I**nventory | Parts stockpile | Unreviewed PRs, unworked backlog items, unread reports |
| **M**otion | Worker movement | Context switching between apps / communication channels |
| **W**aiting | Machine idle | Waiting for approvals, waiting for data, waiting for decisions |
| **O**verproduction | Making more than needed | Features built that weren't validated |
| **O**verprocessing | Extra steps | 6-step approval for $200 purchase |
| **D**efects | Rework | Bug fixes, incorrect specs, miscommunicated requirements |
| **S**kills | Underutilized talent | Senior engineers doing manual QA |
**Exercise:** For your most important process, walk through each waste category and estimate hours/week wasted. This exercise typically reveals 20–40% improvement opportunities in the first pass.
### Cycle Time and Lead Time
**Lead time:** Time from when a request enters the system to when it exits (customer perspective).
**Cycle time:** Time a unit of work is actively being worked on (team perspective).
```
Lead Time = Cycle Time + Wait Time
```
Most teams only measure cycle time. Customers only experience lead time. The gap between the two is pure waste.
**Measuring in your context:**
- Engineering: Lead time = ticket created → in production. Cycle time = in progress → PR merged.
- Sales: Lead time = lead created → closed won. Cycle time = demo completed → proposal sent.
- CS: Lead time = ticket opened → customer confirms resolved. Cycle time = ticket in-progress → resolution sent.
**Improvement pattern:**
1. Measure lead time (not just cycle time)
2. Find the steps where tickets sit waiting
3. Remove the wait (automation, reduced approval layers, clearer handoff criteria)
### WIP Limits
Work-In-Progress limits prevent the multi-tasking trap. When people work on 5 things simultaneously, each thing takes 5x longer and quality drops.
**Recommended WIP limits:**
- Individual IC: 2–3 active items at once
- Team sprint: WIP = number of engineers × 1.5
- Leadership team: No more than 3 company-level priorities per quarter
**Implementation:** In Jira/Linear, add a WIP column. Set a hard limit. When the column is full, no new work starts until something ships.
---
## Part 3: Process Mapping Techniques
### When to Map a Process
Map a process when:
- It's done by more than 2 people
- It fails regularly (errors, rework, complaints)
- It needs to scale (you're about to add people or volume)
- You're automating it (you must understand the manual process first)
- You're onboarding someone new to it
Don't map processes that are genuinely ad-hoc, one-person, or will change significantly in the next 90 days.
### The Three Levels of Process Maps
**Level 1: Swim Lane Map (for cross-functional processes)**
Best for: Customer onboarding, sales-to-CS handoff, escalation handling, hiring
```
Example: Sales to CS Handoff
| Sales AE | Sales Ops | CS Manager | CS Rep |
--------|---------------|---------------|---------------|---------------|
Step 1 | Close deal | | | |
Step 2 | Fill handoff | | | |
| doc | | | |
Step 3 | | Route to CS | | |
Step 4 | | | Review & | |
| | | assign | |
Step 5 | | | | Send welcome |
Step 6 | | | | Schedule kick-|
| | | | off |
```
**Level 2: Flowchart (for decision-heavy processes)**
Best for: Escalation routing, incident response, approval workflows
Use standard symbols:
- Rectangle = action/task
- Diamond = decision (yes/no branch)
- Oval = start/end
- Parallelogram = input/output
**Level 3: Work Instructions (for execution-level processes)**
Best for: Checklists, SOPs, how-to guides
Format:
```
Process: [Name]
Owner: [Role]
Last reviewed: [Date]
Trigger: [What starts this process]
Step 1: [Action] — [Who does it] — [Tool used] — [Expected output]
Step 2: ...
Exceptions:
- If [condition], then [alternative action]
Done when: [Definition of done]
```
### Process Audit Technique
Run this quarterly on your most critical processes:
**1. Walk the process** — Literally follow a unit of work from start to finish. Ask the people doing it, not the people managing it.
**2. Measure three numbers:**
- How long does it actually take? (lead time)
- How often does it go wrong? (error/rework rate)
- What's the cost of a failure? (downstream impact)
**3. Score it:**
```
PROCESS HEALTH SCORE:
Lead time vs. target: [+2 on target / 0 delayed / -2 significantly delayed]
Error rate: [+2 <5% / 0 5-15% / -2 >15%]
Documented: [+1 yes / -1 no]
Owner named: [+1 yes / -1 no]
Last reviewed (< 6 months): [+1 yes / -1 no]
Max: 7. Score <3 = needs immediate attention.
```
---
## Part 4: Automation Decision Framework
### The "Should I Automate This?" Test
Not everything should be automated. Bad automation of a broken process = faster broken process.
**The five-question filter:**
1. **Is the process stable?** If it changes monthly, automate later. Automating unstable processes locks in the wrong behavior.
2. **How often does it happen?** Weekly or more frequent = good candidate. Monthly or less = probably not worth it.
3. **What's the error rate without automation?** If the manual process is accurate 95%+ of the time, automation ROI is lower.
4. **What's the cost of failure?** Customer-facing, compliance, or financial processes deserve higher automation priority than internal reporting.
5. **Is the process well-documented?** If you can't describe it in a flowchart, you can't automate it. Document first.
### Automation ROI Calculation
```
Annual hours saved = (minutes per occurrence / 60) × occurrences per year
Annual labor cost saved = hours saved × fully-loaded cost per hour
Net annual value = labor cost saved + error reduction value + speed improvement value
Build/buy cost = development time + maintenance overhead
Payback period = build/buy cost ÷ net annual value
Rule of thumb: automate if payback period < 12 months
```
**Example:**
- Process: Weekly sales report compilation
- Time: 3 hours/week manually
- Fully-loaded cost: $75/hour
- Annual manual cost: 3 × 52 × $75 = $11,700
- Automation cost: 40 hours to build = $3,000
- Payback: 3,000 ÷ 11,700 = 3 months → **Automate**
### Automation Tiers
**Tier 1: No-code automation** (0–8 hours to implement)
- Tools: Zapier, Make (Integromat), n8n, HubSpot workflows
- Use for: Notification triggers, data syncs between tools, simple conditional routing
- Example: New customer in CRM → create CS ticket → send welcome Slack message
**Tier 2: Low-code automation** (8–40 hours to implement)
- Tools: Retool, internal scripts, Google Apps Script, Airtable Automations
- Use for: Internal dashboards, data transformation, approval workflows
- Example: Weekly metrics compilation from Salesforce + Mixpanel + HubSpot into Notion dashboard
**Tier 3: Engineered automation** (40+ hours to implement)
- Built by engineering team as product/infrastructure work
- Use for: Customer-facing workflows, compliance-critical processes, high-volume operations
- Example: Automated customer health score calculation → CS alert → playbook trigger
### Automation Prioritization Matrix
```
HIGH FREQUENCY
|
Tier 1 now | Tier 2-3 now
(quick win) | (high-value)
|
LOW VALUE ________________|________________ HIGH VALUE
|
Don't bother | Plan for later
| (when it's bigger)
|
LOW FREQUENCY
```
Place each manual process in the quadrant. Execute top-right first, Tier 1 items second.
### Automation Governance
As automation grows, it needs governance:
**Automation registry:** Maintain a list of all automations with:
- Name and description
- Owner (person responsible if it breaks)
- Tools used
- Trigger and action
- Last tested date
- Business impact if down
**Review cadence:** Quarterly review of automation registry. Kill automations nobody uses.
**Failure alerting:** Every production automation must have failure notifications sent to a named owner. Silent failures are worse than no automation.
---
## Part 5: Change Management for Process Rollouts
### Why Process Changes Fail
Most process changes fail not because the process is wrong, but because of how it's rolled out. Common failure modes:
- **Top-down dictate:** Process designed by leadership, announced to team, implemented poorly because people weren't involved and don't understand why.
- **No training:** "Here's the new process" with no demonstration or practice.
- **No feedback loop:** Process is rolled out and never adjusted based on what the team discovers.
- **No accountability:** Process is optional in practice because there are no consequences for ignoring it.
- **Old behavior still possible:** You introduce a new tool but don't turn off the old way.
### The Change Management Framework (ADKAR)
ADKAR (Awareness, Desire, Knowledge, Ability, Reinforcement) is the most practical model for operational change.
**A — Awareness:** Does everyone understand WHY the change is needed?
- Don't just announce the new process — explain what was broken about the old one
- Share the data: "Our current onboarding takes 45 days, customers who onboard faster have 2x better retention. The new process targets 21 days."
**D — Desire:** Do people want to change?
- Resistance is information. Listen to it.
- Involve front-line workers in process design. People support what they help build.
- Address WIIFM (What's In It For Me) for each affected group
**K — Knowledge:** Do people know HOW to do the new process?
- Write it down (work instructions format above)
- Run live demos and practice sessions
- Create a "first time" checklist
**A — Ability:** Can people actually do the new process?
- Identify where people get stuck (first 2 weeks of rollout)
- Have a designated expert for questions
- Remove friction: if the new process requires 3 clicks where the old required 1, people will revert
**R — Reinforcement:** Does the change stick?
- Measure adoption (are people actually using the new process?)
- Celebrate early adopters
- Address non-adoption promptly — call it out without shame
### Change Rollout Checklist
```
PRE-LAUNCH:
□ Process designed and documented
□ Stakeholders identified (people affected by change)
□ Champions identified (people who will help adoption)
□ Training materials created
□ Success metrics defined (how will you know it worked?)
□ Rollback plan documented (what if it breaks something?)
□ Launch timeline set and communicated
LAUNCH WEEK:
□ Announcement sent with WHY, WHAT, and WHEN
□ Training sessions held (at least 2 options for different schedules)
□ Feedback channel opened (Slack thread, form, or dedicated meeting)
□ Champions briefed to support peers
2-WEEK CHECK:
□ Adoption rate measured
□ Friction points documented
□ Quick fixes implemented
□ Feedback reviewed and responded to
30-DAY REVIEW:
□ Success metrics reviewed vs. baseline
□ Process adjustments made based on learnings
□ Champions recognized
□ Process documentation updated with lessons learned
90-DAY CLOSE:
□ Full adoption confirmed or non-adoption addressed
□ Process owners confirmed
□ Handoff to BAU (business as usual) operations
```
### Managing Resistance
**Types of resistance and responses:**
| Resistance Type | What It Sounds Like | Right Response |
|----------------|---------------------|----------------|
| Legitimate concern | "This process won't work because X happens" | Acknowledge, investigate, fix or explain |
| Anxiety | "I don't know how to do this" | Training, support, reassurance |
| Loss of control | "This takes away my judgment" | Involve them in design; give them ownership of part of it |
| Passive non-compliance | Silent ignoring of the new process | Direct conversation; make it visible and required |
| Organizational inertia | "We've always done it this way" | Show the cost of the status quo in concrete terms |
**The three levers of adoption:**
1. **Make the new way easier than the old way** (remove the old path if possible)
2. **Make non-adoption visible** (dashboards showing who's using the process)
3. **Connect process to meaningful outcomes** (show how it affects things people care about)
### Process Documentation Standards
Every process should have exactly one owner responsible for keeping it current.
**Minimum documentation for any process:**
- **Process name** and one-sentence purpose
- **Owner:** Named individual, not a team
- **Trigger:** What starts this process
- **Steps:** Written at the level that a new employee could execute
- **Exceptions:** Common edge cases and how to handle them
- **Done definition:** How you know the process is complete
- **Review date:** Set a future date when this gets reviewed
**Documentation debt kills scale.** The most valuable time to document is right after you've run the process for the third time — you've found the edge cases, you know the real steps, and the process is still fresh.
---
## Framework Selection Guide
| Situation | Framework |
|-----------|-----------|
| We're slow and can't figure out why | Theory of Constraints — find the bottleneck |
| We have lots of waste and overhead | Lean — waste audit (TIMWOODS) |
| Process is inconsistent across team | Process mapping — Level 1 swim lane |
| Deciding what to automate | Automation decision framework + ROI calc |
| New process keeps getting ignored | ADKAR change management |
| Unclear who's responsible | RACI or DRI framework |
| Too many decisions escalating to leadership | RAPID decision rights |
---
*Frameworks synthesized from: Eliyahu Goldratt's The Goal and Critical Chain; Womack and Jones' Lean Thinking; Prosci ADKAR model; Scaled Agile Framework (SAFe) process guidance; operational playbooks from Stripe, Airbnb, and Shopify operations teams.*
FILE:references/scaling_playbook.md
# Scaling Playbook: What Breaks at Each Growth Stage
> Compiled from patterns across 100+ high-growth companies. Not theory — this is what actually breaks and what to do about it.
---
## How to Use This Playbook
Each stage section covers:
1. **What breaks** — the specific failure modes that kill companies at this stage
2. **Hiring** — who to bring in and when
3. **Process** — what to formalize vs. keep loose
4. **Tools** — infrastructure that unlocks the next stage
5. **Communication** — how information flow changes
6. **Culture** — what to protect and what to let go
**Benchmarks are medians** — your mileage varies by sector, geography, and business model.
---
## Stage 0: Pre-Seed / Seed ($0–$2M ARR, 1–15 people)
### Key Benchmarks
| Metric | Benchmark |
|--------|-----------|
| Revenue per employee | $0–$100K (still finding PMF) |
| Manager:IC ratio | N/A (no managers) |
| Burn multiple | 2–5x (acceptable) |
| Runway | 12–18 months minimum |
| Time-to-hire | 2–4 weeks |
### What Breaks
**Premature process.** The #1 mistake at seed stage is adding process before you have a repeatable model. Sprint ceremonies, OKR frameworks, and performance reviews are all theater when you haven't found PMF. Every hour spent in process is an hour not spent learning.
**Wrong first hires.** Hiring "senior" people who've only worked in structured environments. You need people who can operate in chaos, not people who expect process to already exist.
**Founder communication bottleneck.** Founders try to be in every decision. Fine at 5 people, fatal at 12. No written decisions means knowledge lives in founders' heads — unscalable.
**Technical debt accepted as strategy.** "We'll fix it later" said about core data models, auth systems, or billing. Later comes at Series A and it costs 3x more to fix.
### Hiring
- **Don't hire for scale you don't have.** Hire for the next 12 months.
- **First 10 hires set culture permanently.** Get them wrong and you'll spend years correcting.
- **Hire athletes, not specialists.** Generalists who can do multiple jobs outperform specialists at this stage.
- **Avoid VP titles early.** Inflated titles block future hires and create expectations you can't meet.
- **Founder-referral bias is real.** Your network is homogeneous. Force diversity early.
**Who to hire first (in rough order):**
1. Engineers who can ship product (2–3 generalists)
2. First sales/GTM if B2B (founder-led sales first, then one closer)
3. Designer/product (often a hybrid)
4. Customer success (often a founder at first)
### Process
**Formalize nothing before PMF.** Literally. Run on Slack, shared docs, and founder judgment.
**After PMF signals appear, formalize only:**
- How you handle customer escalations
- How you deploy code (even basic CI/CD)
- How you onboard new hires (a 1-page checklist is enough)
**Decision rule:** If a founder has to answer the same question three times, write it down. Once.
### Tools
| Function | Seed-Stage Tool |
|----------|----------------|
| Communication | Slack + Google Workspace |
| Project tracking | Linear or Notion (pick one, stay consistent) |
| CRM | HubSpot free or Notion |
| Engineering | GitHub + basic CI (GitHub Actions) |
| Finance | Brex/Mercury + QuickBooks |
| HR | Rippling or Gusto (basic) |
| Analytics | Mixpanel or PostHog (free tier) |
**Rule:** One tool per function. No tool sprawl. Every extra tool is a coordination tax.
### Communication
- **Weekly all-hands** (30 min max). What shipped, what's stuck, what's next.
- **No status meetings.** Anyone can see status in Linear/Notion.
- **Founder write-ups.** Every major decision gets a 1-paragraph Slack post explaining *why*.
- **Group chat discipline.** One channel per project/customer. Inbox zero mentality.
### Culture
**What to build deliberately:**
- High ownership: everyone acts like they own the company, because they do
- Direct feedback: brutal honesty delivered with care
- Bias to ship: done > perfect
- Customer obsession: founders talk to customers weekly
**What to watch for:**
- "Hero culture" where one person saves everything — unsustainable
- Over-indexing on culture fit (code for homogeneity)
- Avoidance of conflict — mistaking silence for agreement
---
## Stage 1: Series A ($2–$10M ARR, 15–50 people)
### Key Benchmarks
| Metric | Benchmark |
|--------|-----------|
| Revenue per employee | $100–$200K |
| Manager:IC ratio | 1:6–1:8 |
| Burn multiple | 1.5–2.5x |
| Sales efficiency (CAC payback) | <18 months |
| Churn (B2B SaaS) | <10% net annual |
| Engineering velocity | Feature shipped every 1–2 weeks |
| Time-to-hire | 4–6 weeks |
| Offer acceptance rate | >80% |
### What Breaks
**Founder-as-manager bottleneck.** At 20+ people, founders can't manage everyone. The first layer of management needs to appear — and it's usually picked wrong (best IC ≠ best manager).
**Tribal knowledge explosion.** "Ask Sarah" stops working when Sarah has 15 things open. Documentation becomes critical — not for bureaucracy, but because institutional knowledge is now a flight risk.
**Sales process fragmentation.** Without a defined sales process, every rep closes differently. You can't train, debug, or scale what you can't see.
**Scope creep in product.** With Series A money comes investor pressure to expand scope. Teams try to build three things at once and ship nothing well.
**Compensation chaos.** Early employees got equity-heavy deals. New hires get market cash. Someone compares, someone gets upset. No comp philosophy = constant re-negotiation.
**Recruiting becomes a job in itself.** Founders can't hire 30 people themselves. First dedicated recruiter needed by 25 people.
### Hiring
**Who to hire at Series A:**
- **Head of Engineering** (if founder is CTO): needs to be an operator, not just an architect
- **First Sales Manager** (when you have 3+ reps): don't promote the best seller
- **HR/People Ops** (generalist, by 30 people): comp, compliance, recruiting coordination
- **Finance** (fractional CFO or strong controller): Series A board needs real numbers
- **Customer Success Lead**: retention is everything at this stage
**Hiring mistakes to avoid:**
- Hiring "big company" execs who need large teams and established process
- Assuming your Series A lead can recruit (they can intro, not close)
- Taking too long — top candidates have 2–3 offers. Move in <2 weeks from first call to offer.
**Leveling:** Build a simple career ladder *before* the compensation complaints start. 3–4 levels per function is enough.
### Process
**What to formalize at Series A:**
1. **Sprint planning** (2-week sprints, public roadmap)
2. **Sales process** (defined stages with entry/exit criteria)
3. **Onboarding** (30/60/90 day plan for each function)
4. **1:1 cadence** (weekly for direct reports, bi-weekly for skip-levels)
5. **Incident response** (P0/P1/P2 definition, on-call rotation)
6. **Quarterly planning** (OKRs or goals framework — keep it lightweight)
**What to keep loose:**
- Internal project process (let teams self-organize)
- Meeting formats (let teams evolve their own rituals)
- Tool selection within approved stack
**Documentation standard:** Write decisions down in a shared wiki. "Decision log" with date, decision, context, owner, and outcome. Takes 5 minutes, saves hours.
### Tools
| Function | Series A Tool |
|----------|--------------|
| Project/Product | Linear + Notion |
| CRM | HubSpot or Salesforce (Starter) |
| Engineering | GitHub + CI/CD pipeline + Sentry |
| HR/People | Rippling or Lattice (performance) |
| Finance | NetSuite or QBO + Brex |
| Analytics | Mixpanel/Amplitude + Looker (or Metabase) |
| Customer Success | Intercom + HubSpot or Zendesk |
| Docs | Notion or Confluence |
### Communication
**Introduce structured communication layers:**
1. **Company all-hands** (monthly, 60 min): CEO share, metrics review, team spotlights, Q&A
2. **Leadership sync** (weekly, 60 min): cross-functional issues, blockers, priorities
3. **Team standups** (async or 15 min daily): what's in progress, what's blocked
4. **1:1s** (weekly): direct report health, career, performance
5. **Written updates** (weekly to investors + board): CEO memo format
**Information hierarchy:** Everyone in the company should know: (1) company goals this quarter, (2) their team's goals, (3) what they personally own. If they don't, your communication structure is broken.
### Culture
**Deliberate culture work starts here.** You're too big for culture to be accidental.
- **Write down values.** Real values with examples of what they look like in action. Not "integrity" — "we tell investors bad news before we tell them good news."
- **Performance management.** First PIPs (Performance Improvement Plans) happen at this stage. Handle them well — the team is watching.
- **Equity culture.** Make sure people understand what their equity is worth in different outcomes. Lack of transparency breeds resentment.
- **First layoff plan.** Even if you never use it, know the criteria. Reactive layoffs destroy trust; plan-based ones (even painful) preserve it.
---
## Stage 2: Series B ($10–$30M ARR, 50–150 people)
### Key Benchmarks
| Metric | Benchmark |
|--------|-----------|
| Revenue per employee | $150–$300K |
| Manager:IC ratio | 1:5–1:7 |
| Burn multiple | 1.0–1.5x |
| CAC payback | <12 months |
| NRR (net revenue retention) | >110% |
| Engineering: Product ratio | ~3:1 |
| Sales: CS ratio | ~3:1 |
| Time-to-hire (senior) | 6–10 weeks |
| Annual attrition | <15% voluntary |
### What Breaks
**Middle management void.** You now have managers managing managers. The "player-coach" model breaks — people can't be ICs and managers simultaneously at this scale. Force the choice.
**Planning misalignment.** Sales promises what product hasn't built. Product builds what customers didn't ask for. Engineering ships what QA didn't test. Fixing this requires cross-functional planning ceremonies.
**Data fragmentation.** Five different versions of "how are we doing." Sales sees Salesforce. Product sees Amplitude. Finance sees spreadsheets. Nobody agrees. You need a single source of truth.
**Process debt.** The Series A processes are starting to creak. Onboarding that worked for 5 hires/quarter doesn't work for 20. Customer escalation paths built for 50 customers fail at 500.
**Cultural fragmentation.** Engineering culture ≠ Sales culture ≠ Support culture. Sub-cultures form. The shared identity you had at 30 people requires active work to maintain at 100.
**The "brilliant jerk" problem.** High performers with bad behavior were tolerated early. Now they're managers with bad behavior, and it's systemic. Act decisively or lose your best people.
### Hiring
**Who to hire at Series B:**
- **COO or VP Operations**: founder is overwhelmed, someone needs to run the machine
- **VP Sales**: first Sales Manager won't scale to 20-rep org
- **VP Marketing**: demand gen and brand need dedicated ownership
- **Dedicated Recruiting**: 2–3 recruiters minimum; you're hiring 30–50 people/year
- **Data/Analytics**: dedicated analyst or data engineer to consolidate reporting
- **Legal counsel**: fractional or in-house; contracts and compliance are getting complex
**The "big company exec" trap.** Series B is when companies hire their first VP from FAANG or a large SaaS company. 60% of these fail within 18 months. They're used to: large teams, established brand, existing process, political navigation. They struggle with: scrappy execution, no support staff, ambiguous direction. Vet explicitly for startup experience.
**Span of control.** At this stage, hold managers to 5–8 direct reports. More than 8 = no time for actual management. Less than 3 = management overhead isn't justified.
### Process
**What to formalize at Series B:**
1. **Quarterly Business Reviews (QBRs)** — every function presents metrics, wins, gaps
2. **Annual planning** — budget, headcount plan, strategic priorities
3. **Cross-functional roadmap alignment** — product/sales/marketing in sync quarterly
4. **Promotion criteria** — written, public, applied consistently
5. **Interview scorecards** — structured interviews with defined rubrics
6. **Change management** — how major process changes get communicated and adopted
7. **Vendor management** — evaluation criteria, approval process, contract management
**SOPs for critical processes:**
- Customer onboarding (if >50 customers)
- Sales handoff from SDR to AE to CS
- Engineering release process
- Incident response playbook
- Contractor/vendor procurement
### Tools
| Function | Series B Tool |
|----------|--------------|
| Project/Product | Jira or Linear (with roadmapping) |
| CRM | Salesforce (full) |
| ERP/Finance | NetSuite |
| HR | Workday or BambooHR + Lattice |
| Analytics | Looker or Tableau + data warehouse |
| Customer Success | Gainsight or ChurnZero |
| Engineering | GitHub Enterprise + full CI/CD + observability |
| Security | 1Password Teams + SSO (Okta) + endpoint management |
### Communication
**At 50+ people, informal communication breaks down.** Information no longer flows naturally — it has to be architected.
**Communication stack:**
- **Monthly all-hands** (90 min): metrics deep-dive, strategy update, team Q&A
- **Weekly leadership team** (90 min): cross-functional priorities, decisions, escalations
- **Bi-weekly skip-levels** (30 min): every manager holds these with their manager's reports
- **Quarterly town halls** (2 hrs): broader context, financial update, roadmap preview
- **Written company update** (bi-weekly): CEO to all-hands via Slack/email
**The information gradient problem.** People at the top know too much. People at the bottom know too little. Fix this with a deliberate "broadcast" culture — any decision affecting more than 5 people gets written up and shared.
### Culture
**Retention becomes an existential issue.** At Series B, you have 50–150 people who've been with you through something hard. They're valuable. And they have options.
- **Career ladders** are non-negotiable by this stage. People leave when they can't see a future.
- **Manager quality** determines retention. Invest in manager training. Run manager effectiveness surveys.
- **Compensation benchmarking** quarterly. If you're more than 10% below market, you're losing people silently.
- **Culture carriers.** Identify the 10–15 people who embody your culture and make them formally responsible for transmitting it. Give them a platform.
---
## Stage 3: Series C ($30–$75M ARR, 150–500 people)
### Key Benchmarks
| Metric | Benchmark |
|--------|-----------|
| Revenue per employee | $200–$400K |
| Manager:IC ratio | 1:5–1:6 |
| Burn multiple | 0.75–1.25x |
| NRR | >115% |
| CAC payback | <9 months |
| Sales cycle (Enterprise) | 60–120 days |
| Engineering team % | 30–40% of headcount |
| Annual attrition target | <12% voluntary |
| Time-to-hire (senior) | 8–12 weeks |
### What Breaks
**Strategy execution gap.** Leadership agrees on strategy. Middle management interprets it differently. ICs execute on their interpretation. By the time work ships, it barely resembles the original strategy. Fix: strategy must cascade in writing with explicit outcomes.
**Process bureaucracy.** The processes you built at Series B start generating bureaucracy. Approval chains lengthen. Simple decisions require three meetings. The antidote is explicit process owners empowered to eliminate friction.
**Org design complexity.** Do you have functional teams (all engineers in one org) or product teams (engineers embedded in product squads)? The answer affects everything: career paths, knowledge sharing, delivery speed. Most companies get this wrong twice before getting it right.
**Geographic complexity.** First international office or remote-heavy team introduces timezone, communication, and culture challenges that don't exist when everyone is in one room.
**Leadership team dysfunction.** Seven VPs who were all individual contributors two years ago are now running $10M+ organizations. Some have grown into it. Some haven't. This is the stage where hard leadership team changes happen.
### Hiring
**Series C hiring is about depth, not breadth.** You have functional coverage — now hire people who go deep within functions.
- **Functional leaders' deputies**: VP Engineering needs a Director of Platform Engineering, Director of Product Engineering, etc.
- **Internal promotions**: 40–60% of leadership roles should be filled internally by now. If you're hiring externally for everything, you've failed at development.
- **Specialists**: Security, data science, UX research, RevOps — functions that were "shared" become dedicated.
- **General Counsel**: Legal volume justifies full-time counsel.
**Headcount planning discipline.** Every hire should have a business case. "The team is busy" is not a business case. "This role will unlock $X in revenue or save Y hours/week" is a business case.
### Process
**Process consolidation.** Audit every process. Kill anything that doesn't have a clear owner and clear outcome. The average Series C company has 40% more process than it needs.
**Key processes to have locked at Series C:**
1. **Annual planning cycle** (strategy → goals → headcount → budget)
2. **Quarterly operating review** (progress against plan, forecast, adjustments)
3. **Product development lifecycle** (discovery → design → build → launch → measure)
4. **Revenue operations** (forecasting, pipeline management, territory planning)
5. **People operations** (performance cycles, promotion cadence, compensation philosophy)
6. **Risk management** (operational, security, compliance, legal)
**Delegation architecture.** At 200+ people, the COO cannot know about every decision. Build explicit decision rights: what decisions require CEO/COO approval vs. VP vs. Director vs. IC.
### Tools
**Consolidate the tech stack.** By Series C, you have tool sprawl. The average 200-person company has 100+ SaaS tools. 40% are redundant. Consolidation saves $200–500K/year and reduces security surface.
**Must-have by Series C:**
- Enterprise SSO (Okta/Google Workspace with MFA everywhere)
- Data warehouse (Snowflake/BigQuery) + BI layer
- HRIS with performance management (Workday, Rippling, BambooHR)
- Revenue intelligence (Gong, Chorus)
- Security tooling (endpoint, SIEM basics, SOC 2 compliance)
### Communication
**Internal comms becomes a function.** You cannot rely on ad-hoc Slack and email at 200+ people. Someone needs to own internal communications.
- **Monthly CEO update** (written, 500 words max): company performance, strategic context, what's next
- **Quarterly all-hands** (2 hrs): comprehensive business review, open Q&A
- **Leadership alignment sessions** (quarterly): leadership team off-site to calibrate on strategy
- **Manager cascade** (after every major announcement): managers brief their teams with tailored context
### Culture
**Culture is now a function, not an instinct.** By Series C, your original culture-carriers are managers or have left. New people joining have never seen how you worked when you were small.
- **Culture explicitly documented** — not a values poster, a behavioral handbook
- **Onboarding redesigned** for culture transmission at scale
- **Manager enablement** — managers are your primary culture delivery mechanism; invest heavily
- **Listening infrastructure** — eNPS quarterly, exit interviews, skip-level feedback — all analyzed systematically
---
## Stage 4: Growth Stage ($75M+ ARR, 500+ people)
### Key Benchmarks
| Metric | Benchmark |
|--------|-----------|
| Revenue per employee | $300–$600K |
| Manager:IC ratio | 1:4–1:6 |
| Burn multiple (path to profitability) | <0.5x |
| NRR | >120% |
| S&M as % of revenue | 25–35% |
| R&D as % of revenue | 15–25% |
| G&A as % of revenue | 8–12% |
| Rule of 40 | >40 (growth rate + profit margin) |
| Annual attrition target | <10% voluntary |
### What Breaks
**Execution at scale.** The larger you are, the harder it is to move fast. The average decision at a 500-person company takes 3x longer than at a 50-person company. This is not inevitable — but fixing it requires explicit investment.
**Internal politics.** Org boundaries create fiefdoms. VPs protect headcount. Teams optimize for their metrics at the expense of company metrics. This is the #1 culture problem at scale.
**Innovation starvation.** The core business is optimized, but new bets are starved of resources. The people working on new initiatives are constrained by processes designed for a mature product. Structural solution required: separate P&L, separate team, different metrics.
**Middle management bloat.** Growth-stage companies often have too many managers and not enough ICs. A manager managing one other manager managing three ICs is a 3-level chain where 2 people add no value. Flatten aggressively.
### Hiring
**You're now competing for talent with FAANG.** Your advantage is mission, equity, and the ability to have impact. Candidates who want to join a Fortune 500 will not join you. Stop trying to attract them.
- **Leadership pipeline**: promote from within at 50%+ for senior roles
- **Talent density over headcount**: 30 strong engineers > 50 average engineers
- **Diverse hiring**: by this stage, lack of diversity is a business problem, not just an ethical one
### Operational Priorities at Scale
1. **Operational efficiency over growth**: headcount growth should lag revenue growth
2. **Process ownership**: every major process has a named owner accountable for outcomes
3. **Quarterly operating model**: budget vs. actual, full P&L transparency to VP level
4. **Automation**: manual operational processes that cost >40 hrs/week should be automated
---
## Cross-Stage Principles
### The Three Things That Kill Companies at Every Stage
1. **Running out of cash before finding the next unlock** — runway management is sacred
2. **Hiring the wrong person for a critical role** — one bad VP can set you back 18 months
3. **Moving too slowly** — market timing matters; perfect is the enemy of shipped
### The Org Design Progression
```
Seed: Flat | Everyone reports to founder | No structure
Series A: Functional pods | First-line managers | Light structure
Series B: Functional departments | VPs emerge | Defined structure
Series C: Business units or product squads | Directors + VPs | Full structure
Growth: Divisional or matrix | EVPs/SVPs | Corporate structure
```
### Revenue per Employee by Function (B2B SaaS benchmarks)
| Function | Series A | Series B | Series C | Growth |
|----------|----------|----------|----------|--------|
| Engineering | $400K | $500K | $600K | $700K |
| Sales | $250K | $350K | $450K | $500K |
| Customer Success | $300K | $400K | $500K | $600K |
| Marketing | $500K | $700K | $900K | $1M+ |
| G&A | $600K | $800K | $1M | $1.2M |
*Revenue per employee = ARR / headcount in function*
### The Management Span Rule
- **Individual contributors being managed**: 1 manager per 6–8 ICs
- **Managers being managed**: 1 director per 4–6 managers
- **Directors being managed**: 1 VP per 3–5 directors
- **VPs being managed**: 1 C-level per 5–8 VPs
Violation of this creates either manager burnout (too wide) or management theater (too narrow).
---
## Red Flags by Stage
| Stage | Red Flag | Likely Cause |
|-------|----------|-------------|
| Seed | Missed 3+ product deadlines | Wrong team or unclear prioritization |
| Series A | Churn >20% | PMF not actually found, or CS underfunded |
| Series B | >6-month sales cycle on SMB | Pricing/packaging problem |
| Series C | NRR <100% | Product-market fit eroding or CS broken |
| Growth | Rule of 40 <20 | Efficiency problem; hiring ahead of revenue |
---
*Sources: Sequoia, a16z operating frameworks; First Round Capital COO benchmarks; SaaStr metrics databases; OpenView SaaS benchmarks; Bain operational maturity models.*
FILE:scripts/okr_tracker.py
#!/usr/bin/env python3
"""
okr_tracker.py — OKR Cascade and Alignment Tracker
Tracks OKR progress from company → department → team level.
Calculates scores, flags at-risk key results, and generates alignment reports.
Scoring: Google's 0.0–1.0 scale (target: 0.6–0.7; hitting 1.0 means goal was too easy)
Usage:
python okr_tracker.py # Runs with sample data
python okr_tracker.py --input okrs.json # Custom OKR data
python okr_tracker.py --input okrs.json --output report.txt
python okr_tracker.py --format json # Machine-readable output
"""
import json
import sys
import argparse
from datetime import datetime, date
from typing import Any
# ---------------------------------------------------------------------------
# Scoring Engine
# ---------------------------------------------------------------------------
# OKR health thresholds (Google-style 0.0–1.0 scale)
SCORE_THRESHOLDS = {
"on_track": 0.70, # Above this: healthy
"at_risk": 0.40, # Between at_risk and on_track: needs attention
# Below at_risk: off track
}
STATUS_LABELS = {
"on_track": "🟢 On Track",
"at_risk": "🟡 At Risk",
"off_track": "🔴 Off Track",
"complete": "✅ Complete",
"not_started": "⬜ Not Started",
}
RISK_LABELS = {
"critical": "🔴 Critical",
"high": "🟠 High",
"medium": "🟡 Medium",
"low": "🟢 Low",
}
def calculate_kr_score(kr: dict) -> float:
"""
Calculate a Key Result's progress score (0.0–1.0).
Supports multiple KR types:
- numeric: current_value / target_value
- percentage: current_pct / target_pct
- milestone: milestone_score (0.0–1.0 provided directly)
- boolean: done (1.0) / not done (0.0)
"""
kr_type = kr.get("type", "numeric")
if kr_type == "boolean":
return 1.0 if kr.get("done", False) else 0.0
elif kr_type == "milestone":
# Milestone KRs have explicit score (0.0–1.0) or count of milestones hit
milestones_total = kr.get("milestones_total", 1)
milestones_hit = kr.get("milestones_hit", 0)
explicit_score = kr.get("score")
if explicit_score is not None:
return max(0.0, min(1.0, float(explicit_score)))
return milestones_hit / milestones_total if milestones_total > 0 else 0.0
elif kr_type == "percentage":
target = kr.get("target_pct", 100)
current = kr.get("current_pct", 0)
baseline = kr.get("baseline_pct", 0)
if target == baseline:
return 0.0
score = (current - baseline) / (target - baseline)
return max(0.0, min(1.0, score))
else: # numeric (default)
target = kr.get("target_value", 0)
current = kr.get("current_value", 0)
baseline = kr.get("baseline_value", 0)
if target == baseline:
return 0.0
# Handle "lower is better" metrics (e.g., churn, response time)
if kr.get("lower_is_better", False):
if current <= target:
return 1.0
improvement = baseline - current
needed = baseline - target
score = improvement / needed if needed != 0 else 0.0
else:
score = (current - baseline) / (target - baseline)
return max(0.0, min(1.0, score))
def get_kr_status(score: float, quarter_progress: float, kr: dict) -> str:
"""
Determine KR status based on score, time elapsed in quarter, and trend.
A KR is at-risk if its score is significantly behind the time elapsed.
E.g., if we're 70% through the quarter but KR is at 30%, it's at risk.
"""
if kr.get("done", False):
return "complete"
# Not started
if score == 0.0 and quarter_progress < 0.1:
return "not_started"
# Check against absolute thresholds
if score >= SCORE_THRESHOLDS["on_track"]:
return "on_track"
# Adjust for time: if we're early in quarter, lower scores are acceptable
adjusted_threshold = SCORE_THRESHOLDS["at_risk"] * (quarter_progress or 0.5)
if score >= max(adjusted_threshold, SCORE_THRESHOLDS["at_risk"]):
return "at_risk"
return "off_track"
def calculate_objective_score(objective: dict, quarter_progress: float) -> dict:
"""
Score an objective based on its key results.
Returns scored objective with KR scores and status.
"""
key_results = objective.get("key_results", [])
if not key_results:
return {**objective, "score": 0.0, "status": "not_started", "key_results_scored": []}
scored_krs = []
for kr in key_results:
score = calculate_kr_score(kr)
status = get_kr_status(score, quarter_progress, kr)
# Calculate time-adjusted gap
expected_score = quarter_progress * 0.85 # Expect 85% of time-proportional progress
gap = expected_score - score
risk_level = _assess_kr_risk(score, status, gap, quarter_progress, kr)
scored_krs.append({
**kr,
"score": round(score, 3),
"score_pct": f"{score * 100:.0f}%",
"status": status,
"status_label": STATUS_LABELS.get(status, status),
"expected_score": round(expected_score, 3),
"gap_vs_expected": round(gap, 3),
"risk_level": risk_level,
"risk_label": RISK_LABELS.get(risk_level, risk_level),
})
# Objective score = weighted average of KR scores
# Weight is explicit in KR data or defaults to equal weight
total_weight = sum(kr.get("weight", 1.0) for kr in key_results)
weighted_score = sum(
kr_scored["score"] * kr.get("weight", 1.0)
for kr_scored, kr in zip(scored_krs, key_results)
)
obj_score = weighted_score / total_weight if total_weight > 0 else 0.0
# Objective status = worst KR status (a chain is only as strong as weakest link)
status_priority = {"off_track": 0, "at_risk": 1, "not_started": 2, "on_track": 3, "complete": 4}
obj_status = min(scored_krs, key=lambda x: status_priority.get(x["status"], 2))["status"]
return {
**objective,
"score": round(obj_score, 3),
"score_pct": f"{obj_score * 100:.0f}%",
"status": obj_status,
"status_label": STATUS_LABELS.get(obj_status, obj_status),
"key_results_scored": scored_krs,
}
def _assess_kr_risk(
score: float,
status: str,
gap: float,
quarter_progress: float,
kr: dict,
) -> str:
"""Assess risk level for a key result."""
if status == "complete" or status == "on_track":
return "low"
weeks_remaining = kr.get("weeks_remaining", max(1, int((1 - quarter_progress) * 13)))
# Critical: off track with <4 weeks left
if status == "off_track" and weeks_remaining <= 4:
return "critical"
# High: significantly behind with limited time
if gap > 0.3 and weeks_remaining <= 6:
return "high"
# High: off track regardless of time
if status == "off_track":
return "high"
# Medium: at risk
if status == "at_risk":
return "medium"
return "low"
# ---------------------------------------------------------------------------
# OKR Cascade and Alignment Analysis
# ---------------------------------------------------------------------------
def build_okr_tree(data: dict, quarter_progress: float) -> dict:
"""
Build scored OKR tree: company → departments → teams.
Returns full hierarchy with scores at every level.
"""
company = data.get("company_okrs", {})
departments = data.get("department_okrs", [])
teams = data.get("team_okrs", [])
# Score company-level OKRs
company_scored = {
"name": company.get("name", "Company"),
"quarter": company.get("quarter", ""),
"objectives": [
calculate_objective_score(obj, quarter_progress)
for obj in company.get("objectives", [])
],
}
# Score department-level OKRs
depts_scored = []
for dept in departments:
dept_objectives = [
calculate_objective_score(obj, quarter_progress)
for obj in dept.get("objectives", [])
]
dept_score = (
sum(o["score"] for o in dept_objectives) / len(dept_objectives)
if dept_objectives else 0.0
)
depts_scored.append({
**dept,
"objectives": dept_objectives,
"overall_score": round(dept_score, 3),
"overall_score_pct": f"{dept_score * 100:.0f}%",
})
# Score team-level OKRs
teams_scored = []
for team in teams:
team_objectives = [
calculate_objective_score(obj, quarter_progress)
for obj in team.get("objectives", [])
]
team_score = (
sum(o["score"] for o in team_objectives) / len(team_objectives)
if team_objectives else 0.0
)
teams_scored.append({
**team,
"objectives": team_objectives,
"overall_score": round(team_score, 3),
"overall_score_pct": f"{team_score * 100:.0f}%",
})
return {
"company": company_scored,
"departments": depts_scored,
"teams": teams_scored,
}
def analyze_alignment(okr_tree: dict) -> dict:
"""
Analyze how team and department OKRs align to company OKRs.
Flags: orphaned OKRs (no company parent), missing coverage (company OKR with no team support).
"""
company_objective_ids = {
obj.get("id") for obj in okr_tree["company"].get("objectives", [])
if obj.get("id")
}
# Collect all alignment references from dept and team OKRs
alignment_map: dict[str, list[str]] = {oid: [] for oid in company_objective_ids}
orphaned = []
all_supporting = []
def check_objectives(objectives: list, owner_name: str, level: str):
for obj in objectives:
supports = obj.get("supports_company_objective_ids", [])
if not supports:
# Check if it's supposed to support something
if obj.get("supports_company_objective_id"):
supports = [obj["supports_company_objective_id"]]
if not supports:
orphaned.append({
"level": level,
"owner": owner_name,
"objective": obj.get("title", obj.get("name", "Unknown")),
"issue": "No link to company objective — may be misaligned or low priority",
})
else:
for cid in supports:
if cid in alignment_map:
alignment_map[cid].append(f"{level}:{owner_name}")
all_supporting.append(cid)
else:
orphaned.append({
"level": level,
"owner": owner_name,
"objective": obj.get("title", obj.get("name", "Unknown")),
"issue": f"References company objective '{cid}' which doesn't exist",
})
for dept in okr_tree["departments"]:
check_objectives(dept["objectives"], dept.get("name", "Unknown Dept"), "Department")
for team in okr_tree["teams"]:
check_objectives(team["objectives"], team.get("name", "Unknown Team"), "Team")
# Find company objectives with no support from below
unsupported = []
for obj in okr_tree["company"].get("objectives", []):
obj_id = obj.get("id")
if obj_id and obj_id not in all_supporting:
unsupported.append({
"objective_id": obj_id,
"objective": obj.get("title", obj.get("name", "Unknown")),
"issue": "No department or team OKR explicitly supports this company objective",
})
coverage_score = (
len(set(all_supporting)) / len(company_objective_ids) * 100
if company_objective_ids else 100
)
return {
"alignment_map": alignment_map,
"orphaned_okrs": orphaned,
"unsupported_company_objectives": unsupported,
"coverage_score_pct": round(coverage_score, 1),
}
def collect_at_risk_krs(okr_tree: dict) -> list[dict]:
"""Collect all at-risk and off-track key results across the full OKR tree."""
at_risk = []
def scan_objectives(objectives: list, owner: str, level: str):
for obj in objectives:
for kr in obj.get("key_results_scored", []):
if kr["status"] in ("at_risk", "off_track"):
at_risk.append({
"level": level,
"owner": owner,
"objective": obj.get("title", obj.get("name", "Unknown")),
"key_result": kr.get("title", kr.get("name", "Unknown")),
"score": kr["score"],
"score_pct": kr["score_pct"],
"status": kr["status"],
"status_label": kr["status_label"],
"risk_level": kr["risk_level"],
"risk_label": kr["risk_label"],
"gap_vs_expected": kr["gap_vs_expected"],
"notes": kr.get("notes", ""),
})
scan_objectives(
okr_tree["company"].get("objectives", []),
okr_tree["company"].get("name", "Company"),
"Company",
)
for dept in okr_tree["departments"]:
scan_objectives(dept["objectives"], dept.get("name", ""), "Department")
for team in okr_tree["teams"]:
scan_objectives(team["objectives"], team.get("name", ""), "Team")
# Sort: off_track before at_risk, then by gap
status_order = {"off_track": 0, "at_risk": 1}
at_risk.sort(key=lambda x: (status_order.get(x["status"], 2), -x.get("gap_vs_expected", 0)))
return at_risk
# ---------------------------------------------------------------------------
# Report Formatter
# ---------------------------------------------------------------------------
def _score_bar(score: float, width: int = 20) -> str:
"""Render a text progress bar for a 0.0–1.0 score."""
filled = round(score * width)
bar = "█" * filled + "░" * (width - filled)
return f"[{bar}] {score * 100:.0f}%"
def format_report(
okr_tree: dict,
alignment: dict,
at_risk_krs: list[dict],
quarter_progress: float,
quarter_label: str,
) -> str:
"""Format full OKR tracking report as plain text."""
lines = []
now = datetime.now().strftime("%Y-%m-%d %H:%M")
company_name = okr_tree["company"].get("name", "Company")
lines.append("=" * 70)
lines.append(f"OKR TRACKING REPORT — {company_name}")
lines.append(f"Quarter: {quarter_label} | Quarter progress: {quarter_progress * 100:.0f}%")
lines.append(f"Generated: {now}")
lines.append("=" * 70)
# --- Executive Summary ---
lines.append("\n📊 EXECUTIVE SUMMARY")
lines.append("-" * 40)
company_objectives = okr_tree["company"].get("objectives", [])
if company_objectives:
company_avg = sum(o["score"] for o in company_objectives) / len(company_objectives)
on_track = sum(1 for o in company_objectives if o["status"] == "on_track")
at_risk = sum(1 for o in company_objectives if o["status"] == "at_risk")
off_track = sum(1 for o in company_objectives if o["status"] == "off_track")
lines.append(f"Company OKR Score: {_score_bar(company_avg)}")
lines.append(f"Objectives: {len(company_objectives)} total — "
f"🟢 {on_track} on track, 🟡 {at_risk} at risk, 🔴 {off_track} off track")
lines.append(f"At-risk KRs (all): {len(at_risk_krs)}")
lines.append(f"Alignment coverage: {alignment['coverage_score_pct']}% of company objectives have team support")
# Overall health assessment
if company_avg >= 0.7:
health = "🟢 HEALTHY — On track for a strong quarter"
elif company_avg >= 0.5:
health = "🟡 CAUTION — Some objectives need attention"
elif company_avg >= 0.3:
health = "🔴 AT RISK — Multiple objectives behind; intervention needed"
else:
health = "🚨 CRITICAL — Quarter in serious jeopardy; executive review required"
lines.append(f"\nOverall Health: {health}")
# --- Company OKRs ---
lines.append("\n\n🏢 COMPANY OKRs")
lines.append("-" * 40)
for obj in company_objectives:
lines.append(f"\n Objective: {obj.get('title', obj.get('name', 'Unknown'))}")
lines.append(f" Owner: {obj.get('owner', 'Unassigned')} | Score: {_score_bar(obj['score'], 15)} {obj['status_label']}")
for kr in obj.get("key_results_scored", []):
risk_marker = f" {kr['risk_label']}" if kr["risk_level"] in ("critical", "high") else ""
lines.append(f"\n KR: {kr.get('title', kr.get('name', 'Unknown'))}")
lines.append(f" Score: {_score_bar(kr['score'], 12)} {kr['status_label']}{risk_marker}")
# Show actual progress
if kr.get("type") == "numeric":
current = kr.get("current_value", "?")
target = kr.get("target_value", "?")
baseline = kr.get("baseline_value", 0)
unit = kr.get("unit", "")
lines.append(f" Progress: {current}{unit} / {target}{unit} (baseline: {baseline}{unit})")
elif kr.get("type") == "percentage":
lines.append(f" Progress: {kr.get('current_pct', '?')}% / {kr.get('target_pct', '?')}%")
elif kr.get("type") == "milestone":
hit = kr.get("milestones_hit", "?")
total = kr.get("milestones_total", "?")
lines.append(f" Milestones: {hit} / {total}")
if kr.get("notes"):
lines.append(f" Note: {kr['notes']}")
# --- Department OKRs ---
lines.append("\n\n🏬 DEPARTMENT OKRs")
lines.append("-" * 40)
for dept in okr_tree["departments"]:
lines.append(f"\n 📁 {dept.get('name', 'Unknown')} | Score: {_score_bar(dept['overall_score'], 15)}")
for obj in dept.get("objectives", []):
lines.append(f"\n Objective: {obj.get('title', obj.get('name', 'Unknown'))}")
lines.append(f" Owner: {obj.get('owner', 'Unassigned')} | {obj['status_label']}")
supports = obj.get("supports_company_objective_ids", [])
if supports:
lines.append(f" Supports: Company Objective(s) {', '.join(supports)}")
for kr in obj.get("key_results_scored", []):
risk_marker = f" {kr['risk_label']}" if kr["risk_level"] in ("critical", "high") else ""
lines.append(f"\n KR: {kr.get('title', kr.get('name', 'Unknown'))}")
lines.append(f" {_score_bar(kr['score'], 10)} {kr['status_label']}{risk_marker}")
# --- Team OKRs ---
if okr_tree["teams"]:
lines.append("\n\n👥 TEAM OKRs")
lines.append("-" * 40)
for team in okr_tree["teams"]:
lines.append(f"\n 📋 {team.get('name', 'Unknown')} | Score: {_score_bar(team['overall_score'], 15)}")
for obj in team.get("objectives", []):
lines.append(f"\n Objective: {obj.get('title', obj.get('name', 'Unknown'))}")
supports = obj.get("supports_company_objective_ids", [])
if supports:
lines.append(f" Supports: {', '.join(supports)}")
for kr in obj.get("key_results_scored", []):
risk_marker = f" {kr['risk_label']}" if kr["risk_level"] in ("critical", "high") else ""
lines.append(
f" • {kr.get('title', kr.get('name', 'Unknown'))}: "
f"{kr['score_pct']} {kr['status_label']}{risk_marker}"
)
# --- At-Risk KRs ---
lines.append("\n\n⚠️ AT-RISK KEY RESULTS (Action Required)")
lines.append("-" * 40)
if not at_risk_krs:
lines.append("✅ No key results currently at risk or off track.")
else:
critical = [kr for kr in at_risk_krs if kr["risk_level"] == "critical"]
high = [kr for kr in at_risk_krs if kr["risk_level"] == "high"]
medium = [kr for kr in at_risk_krs if kr["risk_level"] == "medium"]
for group_label, group in [("🔴 CRITICAL", critical), ("🟠 HIGH", high), ("🟡 MEDIUM", medium)]:
if not group:
continue
lines.append(f"\n{group_label} ({len(group)} items):")
for kr in group:
lines.append(f"\n [{kr['level']}] {kr['owner']}")
lines.append(f" Obj: {kr['objective']}")
lines.append(f" KR: {kr['key_result']}")
lines.append(f" Score: {kr['score_pct']} {kr['status_label']} (gap vs expected: {kr['gap_vs_expected'] * 100:.0f}pp)")
if kr["notes"]:
lines.append(f" Note: {kr['notes']}")
# --- Alignment Report ---
lines.append("\n\n🔗 ALIGNMENT REPORT")
lines.append("-" * 40)
lines.append(f"Alignment coverage: {alignment['coverage_score_pct']}% of company objectives have explicit support\n")
# Show alignment map
lines.append("Company Objective Coverage:")
for obj in company_objectives:
obj_id = obj.get("id", "")
supporters = alignment["alignment_map"].get(obj_id, [])
obj_name = obj.get("title", obj.get("name", obj_id))
count = len(supporters)
marker = "✅" if count > 0 else "⚠️ "
lines.append(f" {marker} [{obj_id}] {obj_name}")
if supporters:
for s in supporters:
lines.append(f" ↑ {s}")
else:
lines.append(f" ↑ (no department or team OKR supports this)")
if alignment["unsupported_company_objectives"]:
lines.append(f"\n⚠️ Unsupported Company Objectives ({len(alignment['unsupported_company_objectives'])}):")
for u in alignment["unsupported_company_objectives"]:
lines.append(f" • [{u['objective_id']}] {u['objective']}")
lines.append(f" → {u['issue']}")
if alignment["orphaned_okrs"]:
lines.append(f"\n⚠️ Orphaned OKRs (not linked to company objectives):")
for o in alignment["orphaned_okrs"]:
lines.append(f" • [{o['level']}] {o['owner']}: {o['objective']}")
lines.append(f" → {o['issue']}")
# --- Recommendations ---
lines.append("\n\n📋 RECOMMENDED ACTIONS")
lines.append("-" * 40)
recs = _generate_recommendations(okr_tree, at_risk_krs, alignment, quarter_progress)
for i, rec in enumerate(recs, 1):
lines.append(f"\n{i}. {rec['title']}")
lines.append(f" {rec['detail']}")
lines.append(f" Owner: {rec['owner']} | When: {rec['when']}")
lines.append("\n" + "=" * 70)
lines.append("END OF REPORT")
lines.append("=" * 70)
return "\n".join(lines)
def _generate_recommendations(
okr_tree: dict,
at_risk_krs: list[dict],
alignment: dict,
quarter_progress: float,
) -> list[dict]:
"""Generate actionable recommendations based on OKR analysis."""
recs = []
# Critical KRs
critical = [kr for kr in at_risk_krs if kr["risk_level"] == "critical"]
if critical:
recs.append({
"title": f"Emergency review: {len(critical)} critical key result(s) need immediate intervention",
"detail": f"Critical KRs: {', '.join(kr['key_result'] for kr in critical[:3])}. "
f"With limited time remaining, these need escalation today.",
"owner": "COO + KR owners",
"when": "This week",
})
# Off-track objectives
off_track_objs = [
o for o in okr_tree["company"].get("objectives", [])
if o["status"] == "off_track"
]
if off_track_objs:
recs.append({
"title": f"Scope reset for {len(off_track_objs)} off-track company objective(s)",
"detail": "When a company objective is off track by mid-quarter, "
"the options are: (1) resource surge, (2) scope reduction, or (3) accept the miss. "
"Choose explicitly — don't let it drift.",
"owner": "CEO + COO",
"when": "Within 1 week",
})
# Alignment gaps
if alignment["coverage_score_pct"] < 80:
recs.append({
"title": "OKR alignment gap — not all company objectives have team support",
"detail": f"Only {alignment['coverage_score_pct']}% of company objectives have explicit team/dept OKRs supporting them. "
"Either add supporting OKRs or acknowledge these objectives are founder-owned.",
"owner": "COO + VPs",
"when": "Next OKR planning cycle",
})
if alignment["orphaned_okrs"]:
recs.append({
"title": f"{len(alignment['orphaned_okrs'])} orphaned OKR(s) with no company objective linkage",
"detail": "Team OKRs that don't connect to company objectives waste capacity. "
"Either link them explicitly or discontinue them.",
"owner": "Team leads + COO",
"when": "OKR review session",
})
# Late quarter: force ranking
if quarter_progress >= 0.67:
at_risk_count = sum(
1 for o in okr_tree["company"].get("objectives", [])
if o["status"] in ("at_risk", "off_track")
)
if at_risk_count > 0:
recs.append({
"title": f"Late quarter: force-rank which at-risk OKRs to save vs. accept as miss",
"detail": f"{at_risk_count} objectives at risk with <{int((1 - quarter_progress) * 13)} weeks left. "
"You cannot save everything. Pick the 1–2 most important and resource them fully. "
"Explicitly accept the others as misses and learn from them.",
"owner": "CEO + COO",
"when": "Immediately",
})
# Measurement gaps
unscored_krs = []
for obj in okr_tree["company"].get("objectives", []):
for kr in obj.get("key_results_scored", []):
if kr["score"] == 0.0 and kr["status"] == "not_started" and quarter_progress > 0.25:
unscored_krs.append(kr.get("title", kr.get("name", "Unknown")))
if unscored_krs:
recs.append({
"title": f"{len(unscored_krs)} key result(s) show no progress past Q1",
"detail": "KRs with zero progress after 25% of quarter has elapsed are either not started, "
"unmeasured, or forgotten. Require owners to update scores this week.",
"owner": "KR owners",
"when": "This week — before next leadership sync",
})
return recs
def format_json_output(okr_tree: dict, alignment: dict, at_risk_krs: list[dict]) -> str:
"""Format analysis as machine-readable JSON."""
return json.dumps(
{
"generated_at": datetime.now().isoformat(),
"company_score": (
sum(o["score"] for o in okr_tree["company"].get("objectives", []))
/ max(1, len(okr_tree["company"].get("objectives", [])))
),
"at_risk_count": len(at_risk_krs),
"alignment_coverage_pct": alignment["coverage_score_pct"],
"objectives": okr_tree["company"].get("objectives", []),
"departments": okr_tree["departments"],
"teams": okr_tree["teams"],
"at_risk_key_results": at_risk_krs,
"alignment": alignment,
},
indent=2,
)
# ---------------------------------------------------------------------------
# Main Entrypoint
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="OKR Cascade and Alignment Tracker — COO Advisor Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--input", "-i", help="Path to JSON OKR data file", default=None)
parser.add_argument("--output", "-o", help="Path to write report (default: stdout)", default=None)
parser.add_argument(
"--format", "-f",
choices=["text", "json"],
default="text",
help="Output format: text (default) or json",
)
parser.add_argument(
"--quarter-progress",
type=float,
default=None,
help="Override quarter progress (0.0–1.0). Default: auto-calculated from quarter dates.",
)
args = parser.parse_args()
if args.input:
try:
with open(args.input, "r") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: Input file not found: {args.input}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
else:
print("No input file specified — running with sample data.\n")
data = SAMPLE_DATA
# Determine quarter progress
if args.quarter_progress is not None:
quarter_progress = args.quarter_progress
else:
quarter_progress = _calculate_quarter_progress(data)
quarter_label = data.get("company_okrs", {}).get("quarter", "Unknown Quarter")
# Run analysis
okr_tree = build_okr_tree(data, quarter_progress)
alignment = analyze_alignment(okr_tree)
at_risk_krs = collect_at_risk_krs(okr_tree)
# Format output
if args.format == "json":
output = format_json_output(okr_tree, alignment, at_risk_krs)
else:
output = format_report(okr_tree, alignment, at_risk_krs, quarter_progress, quarter_label)
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"Report written to: {args.output}")
else:
print(output)
def _calculate_quarter_progress(data: dict) -> float:
"""Auto-calculate quarter progress from start/end dates in data, or default to 0.5."""
q = data.get("company_okrs", {})
start_str = q.get("quarter_start")
end_str = q.get("quarter_end")
if not start_str or not end_str:
return 0.5 # Default to mid-quarter if not specified
try:
start = date.fromisoformat(start_str)
end = date.fromisoformat(end_str)
today = date.today()
total_days = (end - start).days
elapsed_days = (today - start).days
progress = elapsed_days / total_days if total_days > 0 else 0.5
return max(0.0, min(1.0, progress))
except (ValueError, TypeError):
return 0.5
# ---------------------------------------------------------------------------
# Sample Data
# ---------------------------------------------------------------------------
SAMPLE_DATA = {
"company_okrs": {
"name": "AcmeSaaS",
"quarter": "Q1 2025",
"quarter_start": "2025-01-01",
"quarter_end": "2025-03-31",
"objectives": [
{
"id": "CO1",
"title": "Achieve breakout revenue growth",
"owner": "CEO",
"key_results": [
{
"id": "CO1-KR1",
"title": "Reach $5M net new ARR",
"type": "numeric",
"baseline_value": 0,
"current_value": 2800000,
"target_value": 5000000,
"unit": "",
"notes": "Strong January, February softer; pipeline looks better for March",
},
{
"id": "CO1-KR2",
"title": "Achieve 115% NRR",
"type": "percentage",
"baseline_pct": 108,
"current_pct": 110,
"target_pct": 115,
"notes": "Expansion motion improved; churn still elevated in SMB segment",
},
{
"id": "CO1-KR3",
"title": "Close 3 enterprise deals (>$150K ACV)",
"type": "numeric",
"baseline_value": 0,
"current_value": 1,
"target_value": 3,
"unit": " deals",
"notes": "1 closed, 2 in late-stage negotiation",
},
],
},
{
"id": "CO2",
"title": "Build a world-class product that customers love",
"owner": "CPO",
"key_results": [
{
"id": "CO2-KR1",
"title": "Increase feature adoption rate to 65% (% of customers using 3+ core features)",
"type": "percentage",
"baseline_pct": 48,
"current_pct": 52,
"target_pct": 65,
"notes": "Onboarding improvements shipped; adoption curve is moving",
},
{
"id": "CO2-KR2",
"title": "Ship the integration platform (milestone)",
"type": "milestone",
"milestones_total": 4,
"milestones_hit": 1,
"milestones": [
"API design complete",
"Internal alpha",
"Beta with 5 customers",
"GA launch",
],
"notes": "API design shipped. Internal alpha delayed 2 weeks.",
},
{
"id": "CO2-KR3",
"title": "NPS score reaches 45",
"type": "numeric",
"baseline_value": 32,
"current_value": 38,
"target_value": 45,
"unit": "",
},
],
},
{
"id": "CO3",
"title": "Build an operationally excellent company",
"owner": "COO",
"key_results": [
{
"id": "CO3-KR1",
"title": "Reduce burn multiple from 1.8x to 1.3x",
"type": "numeric",
"baseline_value": 1.8,
"current_value": 1.65,
"target_value": 1.3,
"lower_is_better": True,
"unit": "x",
},
{
"id": "CO3-KR2",
"title": "Achieve <30-day customer onboarding (avg)",
"type": "numeric",
"baseline_value": 47,
"current_value": 38,
"target_value": 30,
"lower_is_better": True,
"unit": " days",
"notes": "Good progress; blocked by technical setup step (avg 12 days)",
},
{
"id": "CO3-KR3",
"title": "Voluntary attrition <10%",
"type": "numeric",
"baseline_value": 15,
"current_value": 12,
"target_value": 10,
"lower_is_better": True,
"unit": "%",
"notes": "2 unexpected departures in January; retention initiatives launched",
},
],
},
],
},
"department_okrs": [
{
"name": "Sales",
"owner": "VP Sales",
"objectives": [
{
"title": "Drive net new ARR to hit company growth target",
"owner": "VP Sales",
"supports_company_objective_ids": ["CO1"],
"key_results": [
{
"title": "Close $4M in new business ARR",
"type": "numeric",
"baseline_value": 0,
"current_value": 2200000,
"target_value": 4000000,
"unit": "",
},
{
"title": "Maintain pipeline coverage ratio ≥3x",
"type": "numeric",
"baseline_value": 2.5,
"current_value": 3.1,
"target_value": 3.0,
"unit": "x",
},
{
"title": "Reduce average sales cycle to 42 days",
"type": "numeric",
"baseline_value": 58,
"current_value": 50,
"target_value": 42,
"lower_is_better": True,
"unit": " days",
},
],
}
],
},
{
"name": "Engineering",
"owner": "VP Engineering",
"objectives": [
{
"title": "Deliver the integration platform on schedule",
"owner": "VP Engineering",
"supports_company_objective_ids": ["CO2"],
"key_results": [
{
"title": "Integration platform beta live with 5 customers",
"type": "milestone",
"milestones_total": 3,
"milestones_hit": 1,
"notes": "Alpha delayed — dependency on API gateway refactor",
},
{
"title": "Deploy frequency ≥10/week",
"type": "numeric",
"baseline_value": 6,
"current_value": 9,
"target_value": 10,
"unit": "/week",
},
{
"title": "P0/P1 incidents <2 per month",
"type": "numeric",
"baseline_value": 5,
"current_value": 2.5,
"target_value": 2,
"lower_is_better": True,
"unit": "/month",
},
],
}
],
},
{
"name": "Customer Success",
"owner": "VP CS",
"objectives": [
{
"title": "Drive retention and expansion to fuel NRR growth",
"owner": "VP CS",
"supports_company_objective_ids": ["CO1", "CO2"],
"key_results": [
{
"title": "Gross retention ≥92%",
"type": "percentage",
"baseline_pct": 88,
"current_pct": 89,
"target_pct": 92,
"notes": "3 at-risk accounts in red status",
},
{
"title": "Average onboarding time ≤30 days",
"type": "numeric",
"baseline_value": 47,
"current_value": 38,
"target_value": 30,
"lower_is_better": True,
"unit": " days",
},
{
"title": "Expansion ARR from existing customers: $800K",
"type": "numeric",
"baseline_value": 0,
"current_value": 580000,
"target_value": 800000,
"unit": "",
},
],
}
],
},
],
"team_okrs": [
{
"name": "Platform Engineering",
"department": "Engineering",
"objectives": [
{
"title": "Build the integration API infrastructure",
"supports_company_objective_ids": ["CO2"],
"key_results": [
{
"title": "API gateway v2 deployed to production",
"type": "boolean",
"done": False,
"notes": "Targeting end of week 8",
},
{
"title": "Webhook system handles 10K events/sec",
"type": "boolean",
"done": False,
},
{
"title": "P99 API latency <200ms",
"type": "numeric",
"baseline_value": 380,
"current_value": 290,
"target_value": 200,
"lower_is_better": True,
"unit": "ms",
},
],
}
],
},
{
"name": "Enterprise Sales Team",
"department": "Sales",
"objectives": [
{
"title": "Land 3 enterprise accounts",
"supports_company_objective_ids": ["CO1"],
"key_results": [
{
"title": "3 enterprise deals closed",
"type": "numeric",
"baseline_value": 0,
"current_value": 1,
"target_value": 3,
"unit": " deals",
},
{
"title": "5 enterprise POCs initiated",
"type": "numeric",
"baseline_value": 0,
"current_value": 4,
"target_value": 5,
"unit": " POCs",
},
],
}
],
},
],
}
if __name__ == "__main__":
main()
FILE:scripts/ops_efficiency_analyzer.py
#!/usr/bin/env python3
"""
ops_efficiency_analyzer.py — Operational Efficiency Analyzer
Analyzes startup operational efficiency using Theory of Constraints,
process maturity scoring, and bottleneck identification.
Usage:
python ops_efficiency_analyzer.py # Runs with sample data
python ops_efficiency_analyzer.py --input data.json # Custom data
python ops_efficiency_analyzer.py --input data.json --output report.txt
Input format: See SAMPLE_DATA at bottom of file.
"""
import json
import sys
import argparse
import math
from datetime import datetime
from typing import Any, Optional
# ---------------------------------------------------------------------------
# Data Models (plain dicts with type aliases for clarity)
# ---------------------------------------------------------------------------
ProcessData = dict[str, Any]
TeamData = dict[str, Any]
MetricsData = dict[str, Any]
# ---------------------------------------------------------------------------
# Process Maturity Scoring
# ---------------------------------------------------------------------------
MATURITY_LEVELS = {
1: "Ad Hoc",
2: "Defined",
3: "Managed",
4: "Optimized",
5: "Innovating",
}
MATURITY_DESCRIPTIONS = {
1: "No documented process. Outcomes depend on individual heroics.",
2: "Process exists and is documented. Inconsistently followed.",
3: "Process is followed consistently. Metrics are tracked.",
4: "Process is optimized based on metrics. Proactively improved.",
5: "Process enables competitive advantage. Continuously innovating.",
}
MATURITY_CRITERIA = {
"documentation": {
"weight": 0.20,
"levels": {
0: "No documentation",
1: "Informal notes or tribal knowledge",
2: "Process documented but not maintained",
3: "Documented, current, accessible",
4: "Documented with examples, edge cases, and owner",
5: "Living doc with version history and improvement log",
},
},
"ownership": {
"weight": 0.15,
"levels": {
0: "No owner",
1: "Unclear ownership, multiple people responsible",
2: "Named team responsible",
3: "Named individual DRI",
4: "DRI with metrics accountability",
5: "DRI with improvement mandate and resources",
},
},
"metrics": {
"weight": 0.20,
"levels": {
0: "No metrics",
1: "Anecdotal measurement",
2: "Some metrics tracked, not regularly reviewed",
3: "Key metrics tracked and reviewed monthly",
4: "Metrics drive decisions, targets set",
5: "Predictive metrics, benchmarked externally",
},
},
"automation": {
"weight": 0.20,
"levels": {
0: "100% manual",
1: "Mostly manual, some tools used",
2: "Key steps automated, significant manual work remains",
3: "Majority automated, manual exception handling",
4: "Mostly automated with exception playbooks",
5: "Fully automated with human oversight only",
},
},
"consistency": {
"weight": 0.15,
"levels": {
0: "Never consistent",
1: "Consistent <50% of time",
2: "Consistent 50-75% of time",
3: "Consistent 75-90% of time",
4: "Consistent >90% of time",
5: "Six Sigma level (>99.7%)",
},
},
"feedback_loop": {
"weight": 0.10,
"levels": {
0: "No feedback loop",
1: "Ad hoc complaints surface issues",
2: "Periodic review when problems arise",
3: "Regular review cadence",
4: "Structured improvement cycles",
5: "Real-time feedback with automated triggers",
},
},
}
def score_process_maturity(process: ProcessData) -> dict[str, Any]:
"""
Score a single process on 1-5 maturity scale.
Returns scored process with dimension breakdown and recommendations.
"""
maturity_inputs = process.get("maturity", {})
total_score = 0.0
dimension_scores = {}
recommendations = []
for dimension, config in MATURITY_CRITERIA.items():
raw_score = maturity_inputs.get(dimension, 0)
# Normalize raw score (0-5) to weight
normalized = (raw_score / 5.0) * config["weight"] * 5
total_score += normalized
dimension_scores[dimension] = raw_score
# Generate recommendation if below threshold
if raw_score < 3:
severity = "🔴 Critical" if raw_score < 2 else "🟡 Needs work"
recommendations.append({
"dimension": dimension,
"current_score": raw_score,
"target_score": 3,
"severity": severity,
"action": _get_improvement_action(dimension, raw_score),
})
# Clamp to 1-5 range (scores can't be below 1 for a running process)
maturity_score = max(1.0, min(5.0, total_score))
maturity_level = round(maturity_score)
return {
"name": process["name"],
"maturity_score": round(maturity_score, 2),
"maturity_level": maturity_level,
"maturity_label": MATURITY_LEVELS[maturity_level],
"dimension_scores": dimension_scores,
"recommendations": recommendations,
"process_data": process,
}
def _get_improvement_action(dimension: str, current_score: int) -> str:
"""Return a concrete improvement action for a given dimension and score."""
actions = {
"documentation": {
0: "Write a basic SOP this week: trigger, steps, owner, done-definition",
1: "Convert tribal knowledge into a written process doc with clear steps",
2: "Assign a process owner to maintain and update documentation quarterly",
},
"ownership": {
0: "Assign a DRI (Directly Responsible Individual) today",
1: "Clarify ownership: assign one named person, remove ambiguity",
2: "Give the named owner accountability for process metrics",
},
"metrics": {
0: "Define 1-2 metrics that measure if this process is working",
1: "Set up automated metric collection and add to monthly review",
2: "Set targets for each metric and review monthly",
},
"automation": {
0: "Identify the highest-volume manual step; automate it first",
1: "Run automation ROI calc — if payback <12 months, build it",
2: "Automate exception routing and error notifications",
},
"consistency": {
0: "Root-cause why the process fails; fix the #1 failure mode",
1: "Create a checklist for the process; require sign-off",
2: "Add process adherence check to team's weekly review",
},
"feedback_loop": {
0: "Add this process to monthly operational review agenda",
1: "Create a feedback channel (Slack thread, form) for process issues",
2: "Set a quarterly review date for this process",
},
}
return actions.get(dimension, {}).get(current_score, "Improve this dimension")
# ---------------------------------------------------------------------------
# Bottleneck Analysis (Theory of Constraints)
# ---------------------------------------------------------------------------
def analyze_bottlenecks(processes: list[ProcessData]) -> dict[str, Any]:
"""
Identify bottlenecks using throughput analysis.
Bottleneck = step with lowest throughput (or highest queue buildup).
"""
bottlenecks = []
throughput_chain = []
for process in processes:
steps = process.get("steps", [])
if not steps:
continue
step_analysis = []
min_throughput = float("inf")
bottleneck_step = None
for step in steps:
throughput = step.get("throughput_per_day", 0)
queue_depth = step.get("current_queue", 0)
avg_wait_hours = step.get("avg_wait_hours", 0)
# Utilization estimate
capacity = step.get("capacity_per_day", throughput * 1.2)
utilization = (throughput / capacity * 100) if capacity > 0 else 100
step_info = {
"name": step["name"],
"throughput_per_day": throughput,
"queue_depth": queue_depth,
"avg_wait_hours": avg_wait_hours,
"utilization_pct": round(utilization, 1),
"is_bottleneck": False,
}
step_analysis.append(step_info)
if throughput < min_throughput:
min_throughput = throughput
bottleneck_step = step_info
if bottleneck_step:
bottleneck_step["is_bottleneck"] = True
# Calculate flow efficiency
total_lead_time = sum(
s.get("avg_wait_hours", 0) + s.get("avg_process_hours", 1)
for s in steps
)
total_process_time = sum(s.get("avg_process_hours", 1) for s in steps)
flow_efficiency = (
(total_process_time / total_lead_time * 100)
if total_lead_time > 0
else 0
)
bottlenecks.append({
"process": process["name"],
"bottleneck_step": bottleneck_step["name"],
"bottleneck_throughput": min_throughput,
"bottleneck_queue": bottleneck_step["queue_depth"],
"flow_efficiency_pct": round(flow_efficiency, 1),
"steps": step_analysis,
"toc_recommendation": _generate_toc_recommendation(
bottleneck_step, process
),
})
throughput_chain.append({
"process": process["name"],
"steps": step_analysis,
})
# Rank bottlenecks by severity (queue depth × utilization)
for b in bottlenecks:
b["severity_score"] = b["bottleneck_queue"] * (b["bottleneck_throughput"] or 1)
bottlenecks.sort(key=lambda x: x["severity_score"], reverse=True)
return {
"bottlenecks": bottlenecks,
"throughput_chain": throughput_chain,
}
def _generate_toc_recommendation(bottleneck_step: dict, process: ProcessData) -> str:
"""Generate a Theory of Constraints recommendation for a bottleneck."""
util = bottleneck_step["utilization_pct"]
queue = bottleneck_step["queue_depth"]
step_name = bottleneck_step["name"]
if util >= 90:
return (
f"ELEVATE: '{step_name}' is at {util}% utilization — at capacity. "
f"Add resources (people, automation, or parallel processing) immediately. "
f"Queue of {queue} units will grow until capacity is increased."
)
elif util >= 70:
return (
f"EXPLOIT: '{step_name}' has capacity headroom but is the constraint. "
f"Eliminate non-value-add work in this step. Protect it from interruptions. "
f"Ensure upstream steps feed it steadily, not in batches."
)
else:
return (
f"INVESTIGATE: '{step_name}' shows low throughput ({bottleneck_step['throughput_per_day']}/day) "
f"despite available capacity. Root cause may be upstream blocking, "
f"unclear handoffs, or quality issues requiring rework."
)
# ---------------------------------------------------------------------------
# Team Structure Analysis
# ---------------------------------------------------------------------------
def analyze_team_structure(team: TeamData) -> dict[str, Any]:
"""
Analyze team structure for span of control, layer count, and hiring gaps.
"""
issues = []
recommendations = []
warnings = []
total_headcount = team.get("total_headcount", 0)
departments = team.get("departments", [])
# Span of control analysis
span_issues = []
for dept in departments:
for manager in dept.get("managers", []):
direct_reports = manager.get("direct_reports", 0)
manages_managers = manager.get("manages_managers", False)
optimal_min = 3 if manages_managers else 5
optimal_max = 5 if manages_managers else 8
if direct_reports < optimal_min:
span_issues.append({
"manager": manager["name"],
"dept": dept["name"],
"reports": direct_reports,
"issue": "Under-span",
"recommendation": f"Merge team or promote ICs — {direct_reports} reports is management overhead",
})
elif direct_reports > optimal_max:
span_issues.append({
"manager": manager["name"],
"dept": dept["name"],
"reports": direct_reports,
"issue": "Over-span",
"recommendation": f"Split team — {direct_reports} reports means minimal 1:1 time and poor feedback loops",
})
# Management layers analysis
max_layers = team.get("management_layers", 0)
expected_layers = _expected_layers(total_headcount)
if max_layers > expected_layers + 1:
issues.append({
"type": "Over-layered",
"detail": f"{max_layers} management layers for {total_headcount} people. "
f"Expected: {expected_layers}. Excess layers slow decisions.",
"recommendation": "Flatten: remove middle management layers that don't add decision value",
})
# Revenue per employee by department
annual_revenue = team.get("annual_revenue_usd", 0)
dept_analysis = []
for dept in departments:
headcount = dept.get("headcount", 0)
if headcount > 0 and annual_revenue > 0:
rev_per_employee = annual_revenue / headcount
benchmark = _dept_revenue_benchmark(dept["name"], team.get("stage", "series_a"))
efficiency_pct = (rev_per_employee / benchmark * 100) if benchmark > 0 else None
dept_analysis.append({
"department": dept["name"],
"headcount": headcount,
"revenue_per_employee": round(rev_per_employee),
"benchmark": benchmark,
"efficiency_vs_benchmark_pct": round(efficiency_pct, 1) if efficiency_pct else "N/A",
"status": _efficiency_status(efficiency_pct),
})
# Open req health
open_reqs = team.get("open_requisitions", 0)
req_to_headcount_ratio = (open_reqs / total_headcount * 100) if total_headcount > 0 else 0
if req_to_headcount_ratio > 20:
warnings.append(
f"High open req ratio: {open_reqs} open reqs against {total_headcount} headcount "
f"({req_to_headcount_ratio:.0f}%). This level of hiring while operating is operationally disruptive."
)
return {
"total_headcount": total_headcount,
"management_layers": max_layers,
"expected_layers": expected_layers,
"span_of_control_issues": span_issues,
"structural_issues": issues,
"department_efficiency": dept_analysis,
"open_req_health": {
"open_reqs": open_reqs,
"ratio_pct": round(req_to_headcount_ratio, 1),
"warnings": warnings,
},
}
def _expected_layers(headcount: int) -> int:
if headcount <= 15:
return 1
elif headcount <= 50:
return 2
elif headcount <= 150:
return 3
elif headcount <= 500:
return 4
else:
return 5
def _dept_revenue_benchmark(dept_name: str, stage: str) -> int:
"""Revenue per employee benchmark by department and stage (USD)."""
benchmarks = {
"series_a": {
"engineering": 400000,
"sales": 250000,
"customer_success": 300000,
"marketing": 500000,
"operations": 400000,
"product": 400000,
"default": 200000,
},
"series_b": {
"engineering": 500000,
"sales": 350000,
"customer_success": 400000,
"marketing": 700000,
"operations": 500000,
"product": 500000,
"default": 300000,
},
"series_c": {
"engineering": 600000,
"sales": 450000,
"customer_success": 500000,
"marketing": 900000,
"operations": 600000,
"product": 600000,
"default": 400000,
},
}
stage_data = benchmarks.get(stage, benchmarks["series_a"])
dept_key = dept_name.lower().replace(" ", "_").replace("-", "_")
return stage_data.get(dept_key, stage_data["default"])
def _efficiency_status(efficiency_pct: Optional[float]) -> str:
if efficiency_pct is None:
return "N/A"
if efficiency_pct >= 90:
return "🟢 On benchmark"
elif efficiency_pct >= 70:
return "🟡 Below benchmark"
else:
return "🔴 Significantly below"
# ---------------------------------------------------------------------------
# Improvement Plan Generator
# ---------------------------------------------------------------------------
def generate_improvement_plan(
process_scores: list[dict],
bottleneck_analysis: dict,
team_analysis: dict,
metrics: MetricsData,
) -> list[dict]:
"""
Generate a prioritized improvement plan combining all analysis outputs.
Priority = Impact × Urgency / Effort
"""
items = []
# Priority 1: Process bottlenecks (Theory of Constraints — fix the constraint first)
for b in bottleneck_analysis.get("bottlenecks", [])[:3]:
items.append({
"priority": 1,
"category": "Bottleneck",
"item": f"Resolve bottleneck in '{b['process']}' at step '{b['bottleneck_step']}'",
"detail": b["toc_recommendation"],
"impact": "HIGH — constraint limits entire system throughput",
"effort": "MEDIUM",
"owner_suggestion": "COO + process owner",
"timebox": "2-4 weeks",
"success_metric": f"Throughput at {b['bottleneck_step']} increases by 25%+",
})
# Priority 2: Critical process maturity gaps
critical_processes = [
p for p in process_scores if p["maturity_score"] < 2.0
]
for proc in sorted(critical_processes, key=lambda x: x["maturity_score"]):
for rec in proc["recommendations"][:2]: # Top 2 recs per critical process
items.append({
"priority": 2,
"category": "Process Maturity",
"item": f"Fix {rec['dimension']} in '{proc['name']}' (score: {rec['current_score']}/5)",
"detail": rec["action"],
"impact": "HIGH — ad-hoc processes create inconsistency and risk",
"effort": "LOW-MEDIUM",
"owner_suggestion": "Process owner",
"timebox": "1-2 weeks",
"success_metric": f"Dimension score improves to 3/5",
})
# Priority 3: Team structural issues
for issue in team_analysis.get("structural_issues", []):
items.append({
"priority": 3,
"category": "Org Structure",
"item": issue["type"],
"detail": issue["detail"],
"impact": "MEDIUM — structural issues compound over time",
"effort": "HIGH",
"owner_suggestion": "COO + People",
"timebox": "1-2 quarters",
"success_metric": "Management layer count normalized",
})
for span_issue in team_analysis.get("span_of_control_issues", []):
severity = "HIGH" if span_issue["issue"] == "Over-span" else "MEDIUM"
items.append({
"priority": 3,
"category": "Span of Control",
"item": f"{span_issue['issue']}: {span_issue['manager']} ({span_issue['dept']})",
"detail": span_issue["recommendation"],
"impact": severity,
"effort": "MEDIUM",
"owner_suggestion": f"VP {span_issue['dept']}",
"timebox": "1 quarter",
"success_metric": "Span within 5-8 for ICs, 3-5 for managers",
})
# Priority 4: Maturity improvements for non-critical processes
medium_processes = [
p for p in process_scores if 2.0 <= p["maturity_score"] < 3.5
]
for proc in sorted(medium_processes, key=lambda x: x["maturity_score"])[:3]:
if proc["recommendations"]:
top_rec = proc["recommendations"][0]
items.append({
"priority": 4,
"category": "Process Improvement",
"item": f"Improve {top_rec['dimension']} in '{proc['name']}'",
"detail": top_rec["action"],
"impact": "MEDIUM",
"effort": "LOW",
"owner_suggestion": "Process owner",
"timebox": "2-4 weeks",
"success_metric": f"Dimension score reaches 3/5",
})
# Priority 5: Metrics-driven flags
burn_multiple = metrics.get("burn_multiple")
if burn_multiple and burn_multiple > 2.0:
items.append({
"priority": 2,
"category": "Financial Efficiency",
"item": f"Burn multiple of {burn_multiple:.1f}x is above healthy range",
"detail": "Burn multiple >1.5x indicates spending exceeds efficient growth. Review headcount-to-revenue ratio by department.",
"impact": "HIGH",
"effort": "MEDIUM",
"owner_suggestion": "COO + CFO",
"timebox": "30 days to diagnose, 60-90 days to act",
"success_metric": "Burn multiple <1.5x within 2 quarters",
})
nrr = metrics.get("net_revenue_retention_pct")
if nrr and nrr < 100:
items.append({
"priority": 1,
"category": "Revenue Health",
"item": f"NRR of {nrr}% — losing more from churn/contraction than gaining from expansion",
"detail": "NRR <100% means the customer base shrinks without new sales. Investigate churn root causes immediately.",
"impact": "CRITICAL",
"effort": "HIGH",
"owner_suggestion": "COO + VP CS",
"timebox": "Immediate — 30 days to root cause, 90 days to fix",
"success_metric": "NRR >100% within 2 quarters",
})
# Sort by priority then impact
priority_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
items.sort(key=lambda x: (x["priority"], priority_order.get(x["impact"].split(" — ")[0], 9)))
return items
# ---------------------------------------------------------------------------
# Report Formatter
# ---------------------------------------------------------------------------
def format_report(
process_scores: list[dict],
bottleneck_analysis: dict,
team_analysis: dict,
improvement_plan: list[dict],
metrics: MetricsData,
) -> str:
"""Format the full analysis report as plain text."""
lines = []
now = datetime.now().strftime("%Y-%m-%d %H:%M")
lines.append("=" * 70)
lines.append("OPERATIONAL EFFICIENCY ANALYSIS REPORT")
lines.append(f"Generated: {now}")
lines.append("=" * 70)
# --- Executive Summary ---
lines.append("\n📊 EXECUTIVE SUMMARY")
lines.append("-" * 40)
avg_maturity = (
sum(p["maturity_score"] for p in process_scores) / len(process_scores)
if process_scores else 0
)
critical_count = sum(1 for p in process_scores if p["maturity_score"] < 2.0)
bottleneck_count = len(bottleneck_analysis.get("bottlenecks", []))
plan_items = len(improvement_plan)
lines.append(f"Average Process Maturity: {avg_maturity:.1f}/5.0 ({MATURITY_LEVELS.get(round(avg_maturity), 'Unknown')})")
lines.append(f"Critical Process Gaps: {critical_count}")
lines.append(f"Active Bottlenecks: {bottleneck_count}")
lines.append(f"Improvement Plan Items: {plan_items}")
if metrics:
lines.append("\nKey Business Metrics:")
if metrics.get("burn_multiple"):
flag = " ⚠️" if metrics["burn_multiple"] > 2.0 else ""
lines.append(f" Burn Multiple: {metrics['burn_multiple']:.1f}x{flag}")
if metrics.get("net_revenue_retention_pct"):
flag = " ⚠️" if metrics["net_revenue_retention_pct"] < 100 else ""
lines.append(f" NRR: {metrics['net_revenue_retention_pct']}%{flag}")
if metrics.get("cac_payback_months"):
flag = " ⚠️" if metrics["cac_payback_months"] > 18 else ""
lines.append(f" CAC Payback: {metrics['cac_payback_months']} months{flag}")
# --- Process Maturity Scores ---
lines.append("\n\n📋 PROCESS MATURITY SCORES")
lines.append("-" * 40)
lines.append(f"{'Process':<35} {'Score':>6} {'Level':<12} {'Status'}")
lines.append(f"{'─'*35} {'─'*6} {'─'*12} {'─'*20}")
for p in sorted(process_scores, key=lambda x: x["maturity_score"]):
score = p["maturity_score"]
label = p["maturity_label"]
status = "🔴 Critical" if score < 2 else ("🟡 Needs work" if score < 3.5 else "🟢 Healthy")
lines.append(f"{p['name']:<35} {score:>6.1f} {label:<12} {status}")
# Dimension heatmap
lines.append("\n\nDimension Breakdown (scores 0-5):")
lines.append(f"{'Process':<30} {'Doc':>4} {'Own':>4} {'Met':>4} {'Aut':>4} {'Con':>4} {'Fbk':>4}")
lines.append(f"{'─'*30} {'─'*4} {'─'*4} {'─'*4} {'─'*4} {'─'*4} {'─'*4}")
for p in sorted(process_scores, key=lambda x: x["maturity_score"]):
d = p["dimension_scores"]
lines.append(
f"{p['name']:<30} {d.get('documentation',0):>4} {d.get('ownership',0):>4} "
f"{d.get('metrics',0):>4} {d.get('automation',0):>4} "
f"{d.get('consistency',0):>4} {d.get('feedback_loop',0):>4}"
)
# --- Bottleneck Analysis ---
lines.append("\n\n🔍 BOTTLENECK ANALYSIS (Theory of Constraints)")
lines.append("-" * 40)
bottlenecks = bottleneck_analysis.get("bottlenecks", [])
if not bottlenecks:
lines.append("No process steps defined for bottleneck analysis.")
else:
for i, b in enumerate(bottlenecks, 1):
lines.append(f"\n{i}. {b['process']}")
lines.append(f" Bottleneck step: {b['bottleneck_step']}")
lines.append(f" Throughput: {b['bottleneck_throughput']}/day")
lines.append(f" Queue depth: {b['bottleneck_queue']} units")
lines.append(f" Flow efficiency: {b['flow_efficiency_pct']}%")
lines.append(f" Recommendation: {b['toc_recommendation']}")
lines.append(f"\n Step-by-step throughput:")
for step in b["steps"]:
marker = " ← BOTTLENECK" if step["is_bottleneck"] else ""
lines.append(
f" {step['name']:<30} {step['throughput_per_day']:>4}/day "
f"Queue: {step['queue_depth']:>4} Util: {step['utilization_pct']:>5.1f}%{marker}"
)
# --- Team Structure ---
lines.append("\n\n👥 TEAM STRUCTURE ANALYSIS")
lines.append("-" * 40)
lines.append(f"Total headcount: {team_analysis['total_headcount']}")
lines.append(f"Management layers: {team_analysis['management_layers']} (expected: {team_analysis['expected_layers']})")
span_issues = team_analysis.get("span_of_control_issues", [])
if span_issues:
lines.append(f"\n⚠️ Span of Control Issues ({len(span_issues)}):")
for issue in span_issues:
lines.append(f" {issue['issue']}: {issue['manager']} ({issue['dept']}) — {issue['reports']} reports")
lines.append(f" → {issue['recommendation']}")
dept_eff = team_analysis.get("department_efficiency", [])
if dept_eff:
lines.append(f"\nDepartment Revenue Efficiency:")
lines.append(f"{'Department':<20} {'HC':>4} {'Rev/Head':>10} {'Benchmark':>10} {'vs Bench':>9} {'Status'}")
lines.append(f"{'─'*20} {'─'*4} {'─'*10} {'─'*10} {'─'*9} {'─'*20}")
for d in dept_eff:
rev = f"," if d['revenue_per_employee'] else "N/A"
bench = f"," if d['benchmark'] else "N/A"
vs_bench = f"{d['efficiency_vs_benchmark_pct']}%" if d['efficiency_vs_benchmark_pct'] != "N/A" else "N/A"
lines.append(
f"{d['department']:<20} {d['headcount']:>4} {rev:>10} {bench:>10} {vs_bench:>9} {d['status']}"
)
# --- Improvement Plan ---
lines.append("\n\n🎯 PRIORITIZED IMPROVEMENT PLAN")
lines.append("-" * 40)
lines.append("Items ranked by priority (1=highest). Fix Priority 1 before starting Priority 2.\n")
current_priority = None
for i, item in enumerate(improvement_plan, 1):
if item["priority"] != current_priority:
current_priority = item["priority"]
lines.append(f"\nPRIORITY {current_priority}")
lines.append("─" * 30)
lines.append(f"\n{i}. [{item['category']}] {item['item']}")
lines.append(f" Detail: {item['detail']}")
lines.append(f" Impact: {item['impact']}")
lines.append(f" Effort: {item['effort']}")
lines.append(f" Owner: {item['owner_suggestion']}")
lines.append(f" Timebox: {item['timebox']}")
lines.append(f" Success: {item['success_metric']}")
lines.append("\n" + "=" * 70)
lines.append("END OF REPORT")
lines.append("=" * 70)
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Main Entrypoint
# ---------------------------------------------------------------------------
def run_analysis(data: dict) -> str:
"""Run the full analysis pipeline on input data."""
processes = data.get("processes", [])
team = data.get("team", {})
metrics = data.get("metrics", {})
# 1. Score process maturity
process_scores = [score_process_maturity(p) for p in processes]
# 2. Analyze bottlenecks
bottleneck_analysis = analyze_bottlenecks(processes)
# 3. Analyze team structure
team_analysis = analyze_team_structure(team)
# 4. Generate improvement plan
improvement_plan = generate_improvement_plan(
process_scores, bottleneck_analysis, team_analysis, metrics
)
# 5. Format and return report
return format_report(
process_scores, bottleneck_analysis, team_analysis, improvement_plan, metrics
)
def main():
parser = argparse.ArgumentParser(
description="Operational Efficiency Analyzer — COO Advisor Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--input", "-i",
help="Path to JSON input file (default: use built-in sample data)",
default=None,
)
parser.add_argument(
"--output", "-o",
help="Path to write report (default: stdout)",
default=None,
)
args = parser.parse_args()
if args.input:
try:
with open(args.input, "r") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: Input file not found: {args.input}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in input file: {e}", file=sys.stderr)
sys.exit(1)
else:
print("No input file specified — running with sample data.\n")
data = SAMPLE_DATA
report = run_analysis(data)
if args.output:
with open(args.output, "w") as f:
f.write(report)
print(f"Report written to: {args.output}")
else:
print(report)
# ---------------------------------------------------------------------------
# Sample Data
# ---------------------------------------------------------------------------
SAMPLE_DATA = {
"company": "AcmeSaaS",
"stage": "series_b",
"metrics": {
"annual_revenue_usd": 18000000,
"burn_multiple": 1.8,
"net_revenue_retention_pct": 108,
"cac_payback_months": 14,
"headcount": 85,
"monthly_churn_pct": 1.2,
},
"processes": [
{
"name": "Customer Onboarding",
"category": "Customer Success",
"maturity": {
"documentation": 3,
"ownership": 4,
"metrics": 3,
"automation": 2,
"consistency": 3,
"feedback_loop": 2,
},
"steps": [
{
"name": "Contract signed → kickoff scheduled",
"throughput_per_day": 4,
"capacity_per_day": 6,
"current_queue": 3,
"avg_wait_hours": 4,
"avg_process_hours": 1,
},
{
"name": "Technical setup & integration",
"throughput_per_day": 2,
"capacity_per_day": 3,
"current_queue": 8,
"avg_wait_hours": 24,
"avg_process_hours": 8,
},
{
"name": "Training & enablement",
"throughput_per_day": 3,
"capacity_per_day": 4,
"current_queue": 2,
"avg_wait_hours": 8,
"avg_process_hours": 4,
},
{
"name": "Go-live confirmation",
"throughput_per_day": 4,
"capacity_per_day": 6,
"current_queue": 1,
"avg_wait_hours": 2,
"avg_process_hours": 1,
},
],
},
{
"name": "Sales Deal Qualification",
"category": "Sales",
"maturity": {
"documentation": 2,
"ownership": 3,
"metrics": 4,
"automation": 2,
"consistency": 2,
"feedback_loop": 3,
},
"steps": [
{
"name": "Inbound lead review",
"throughput_per_day": 15,
"capacity_per_day": 20,
"current_queue": 5,
"avg_wait_hours": 2,
"avg_process_hours": 0.5,
},
{
"name": "BANT qualification call",
"throughput_per_day": 8,
"capacity_per_day": 10,
"current_queue": 12,
"avg_wait_hours": 24,
"avg_process_hours": 1,
},
{
"name": "Demo scheduling & prep",
"throughput_per_day": 6,
"capacity_per_day": 8,
"current_queue": 4,
"avg_wait_hours": 8,
"avg_process_hours": 0.5,
},
],
},
{
"name": "Engineering Deployment",
"category": "Engineering",
"maturity": {
"documentation": 4,
"ownership": 5,
"metrics": 4,
"automation": 4,
"consistency": 5,
"feedback_loop": 4,
},
"steps": [
{
"name": "PR submitted",
"throughput_per_day": 20,
"capacity_per_day": 25,
"current_queue": 8,
"avg_wait_hours": 3,
"avg_process_hours": 2,
},
{
"name": "Code review",
"throughput_per_day": 18,
"capacity_per_day": 22,
"current_queue": 10,
"avg_wait_hours": 4,
"avg_process_hours": 1,
},
{
"name": "CI pipeline",
"throughput_per_day": 18,
"capacity_per_day": 30,
"current_queue": 2,
"avg_wait_hours": 0.5,
"avg_process_hours": 0.5,
},
{
"name": "Deploy to production",
"throughput_per_day": 16,
"capacity_per_day": 20,
"current_queue": 1,
"avg_wait_hours": 0.5,
"avg_process_hours": 0.25,
},
],
},
{
"name": "Incident Response",
"category": "Engineering / Operations",
"maturity": {
"documentation": 2,
"ownership": 2,
"metrics": 1,
"automation": 1,
"consistency": 2,
"feedback_loop": 1,
},
"steps": [],
},
{
"name": "Employee Onboarding",
"category": "People",
"maturity": {
"documentation": 2,
"ownership": 2,
"metrics": 1,
"automation": 1,
"consistency": 2,
"feedback_loop": 2,
},
"steps": [],
},
{
"name": "Vendor Procurement",
"category": "Operations",
"maturity": {
"documentation": 1,
"ownership": 1,
"metrics": 0,
"automation": 0,
"consistency": 1,
"feedback_loop": 0,
},
"steps": [],
},
],
"team": {
"total_headcount": 85,
"annual_revenue_usd": 18000000,
"stage": "series_b",
"management_layers": 3,
"open_requisitions": 18,
"departments": [
{
"name": "Engineering",
"headcount": 32,
"managers": [
{"name": "VP Engineering", "direct_reports": 4, "manages_managers": True},
{"name": "Engineering Manager (Platform)", "direct_reports": 7, "manages_managers": False},
{"name": "Engineering Manager (Product)", "direct_reports": 8, "manages_managers": False},
{"name": "Engineering Manager (Infra)", "direct_reports": 9, "manages_managers": False},
],
},
{
"name": "Sales",
"headcount": 18,
"managers": [
{"name": "VP Sales", "direct_reports": 3, "manages_managers": True},
{"name": "Sales Manager (SMB)", "direct_reports": 6, "manages_managers": False},
{"name": "Sales Manager (Enterprise)", "direct_reports": 4, "manages_managers": False},
],
},
{
"name": "Customer Success",
"headcount": 12,
"managers": [
{"name": "VP CS", "direct_reports": 2, "manages_managers": False},
],
},
{
"name": "Marketing",
"headcount": 8,
"managers": [
{"name": "VP Marketing", "direct_reports": 7, "manages_managers": False},
],
},
{
"name": "Operations",
"headcount": 6,
"managers": [
{"name": "COO", "direct_reports": 5, "manages_managers": True},
],
},
{
"name": "Product",
"headcount": 9,
"managers": [
{"name": "VP Product", "direct_reports": 8, "manages_managers": False},
],
},
],
},
}
if __name__ == "__main__":
main()
Provides strategic business advice by channelling the perspectives of 10 executive roles — CEO, CTO, COO, CPO, CMO, CFO, CRO, CISO, CHRO, and Executive Mento...
---
name: "c-level-advisor"
description: "Provides strategic business advice by channelling the perspectives of 10 executive roles — CEO, CTO, COO, CPO, CMO, CFO, CRO, CISO, CHRO, and Executive Mentor — across decisions, trade-offs, and org challenges. Runs multi-role board meetings, routes questions to the right executive voice, and delivers structured recommendations (Bottom Line → What → Why → How to Act → Your Decision). Use when a founder or executive needs business strategy advice, leadership perspective, executive decision support, board-level input, fundraising guidance, product-market fit review, hiring or culture frameworks, risk assessment, or competitive analysis."
license: MIT
metadata:
version: 2.0.0
author: Alireza Rezvani
category: c-level
domain: executive-advisory
updated: 2026-03-05
skills_count: 28
scripts_count: 25
references_count: 52
---
# C-Level Advisory Ecosystem
A complete virtual board of directors for founders and executives.
## Quick Start
```
1. Run /cs:setup → creates company-context.md (all agents read this)
✓ Verify company-context.md was created and contains your company name,
stage, and core metrics before proceeding.
2. Ask any strategic question → Chief of Staff routes to the right role
3. For big decisions → /cs:board triggers a multi-role board meeting
✓ Confirm at least 3 roles have weighed in before accepting a conclusion.
```
### Commands
#### `/cs:setup` — Onboarding Questionnaire
Walks through the following prompts and writes `company-context.md` to the project root. Run once per company or when context changes significantly.
```
Q1. What is your company name and one-line description?
Q2. What stage are you at? (Idea / Pre-seed / Seed / Series A / Series B+)
Q3. What is your current ARR (or MRR) and runway in months?
Q4. What is your team size and structure?
Q5. What industry and customer segment do you serve?
Q6. What are your top 3 priorities for the next 90 days?
Q7. What is your biggest current risk or blocker?
```
After collecting answers, the agent writes structured output:
```markdown
# Company Context
- Name: <answer>
- Stage: <answer>
- Industry: <answer>
- Team size: <answer>
- Key metrics: <ARR/MRR, growth rate, runway>
- Top priorities: <answer>
- Key risks: <answer>
```
#### `/cs:board` — Full Board Meeting
Convenes all relevant executive roles in three phases:
```
Phase 1 — Framing: Chief of Staff states the decision and success criteria.
Phase 2 — Isolation: Each role produces independent analysis (no cross-talk).
Phase 3 — Debate: Roles surface conflicts, stress-test assumptions, align on
a recommendation. Dissenting views are preserved in the log.
```
Use for high-stakes or cross-functional decisions. Confirm at least 3 roles have weighed in before accepting a conclusion.
### Chief of Staff Routing Matrix
When a question arrives without a role prefix, the Chief of Staff maps it to the appropriate executive using these primary signals:
| Topic Signal | Primary Role | Supporting Roles |
|---|---|---|
| Fundraising, valuation, burn | CFO | CEO, CRO |
| Architecture, build vs. buy, tech debt | CTO | CPO, CISO |
| Hiring, culture, performance | CHRO | CEO, Executive Mentor |
| GTM, demand gen, positioning | CMO | CRO, CPO |
| Revenue, pipeline, sales motion | CRO | CMO, CFO |
| Security, compliance, risk | CISO | CTO, CFO |
| Product roadmap, prioritisation | CPO | CTO, CMO |
| Ops, process, scaling | COO | CFO, CHRO |
| Vision, strategy, investor relations | CEO | Executive Mentor |
| Career, founder psychology, leadership | Executive Mentor | CEO, CHRO |
| Multi-domain / unclear | Chief of Staff convenes board | All relevant roles |
### Invoking a Specific Role Directly
To bypass Chief of Staff routing and address one executive directly, prefix your question with the role name:
```
CFO: What is our optimal burn rate heading into a Series A?
CTO: Should we rebuild our auth layer in-house or buy a solution?
CHRO: How do we design a performance review process for a 15-person team?
```
The Chief of Staff still logs the exchange; only routing is skipped.
### Example: Strategic Question
**Input:** "Should we raise a Series A now or extend runway and grow ARR first?"
**Output format:**
- **Bottom Line:** Extend runway 6 months; raise at $2M ARR for better terms.
- **What:** Current $800K ARR is below the threshold most Series A investors benchmark.
- **Why:** Raising now increases dilution risk; 6-month extension is achievable with current burn.
- **How to Act:** Cut 2 low-ROI channels, hit $2M ARR, then run a 6-week fundraise sprint.
- **Your Decision:** Proceed with extension / Raise now anyway (choose one).
### Example: company-context.md (after /cs:setup)
```markdown
# Company Context
- Name: Acme Inc.
- Stage: Seed ($800K ARR)
- Industry: B2B SaaS
- Team size: 12
- Key metrics: 15% MoM growth, 18-month runway
- Top priorities: Series A readiness, enterprise GTM
```
## What's Included
### 10 C-Suite Roles
CEO, CTO, COO, CPO, CMO, CFO, CRO, CISO, CHRO, Executive Mentor
### 6 Orchestration Skills
Founder Onboard, Chief of Staff (router), Board Meeting, Decision Logger, Agent Protocol, Context Engine
### 6 Cross-Cutting Capabilities
Board Deck Builder, Scenario War Room, Competitive Intel, Org Health Diagnostic, M&A Playbook, International Expansion
### 6 Culture & Collaboration
Culture Architect, Company OS, Founder Coach, Strategic Alignment, Change Management, Internal Narrative
## Key Features
- **Internal Quality Loop:** Self-verify → peer-verify → critic pre-screen → present
- **Two-Layer Memory:** Raw transcripts + approved decisions only (prevents hallucinated consensus)
- **Board Meeting Isolation:** Phase 2 independent analysis before cross-examination
- **Proactive Triggers:** Context-driven early warnings without being asked
- **Structured Output:** Bottom Line → What → Why → How to Act → Your Decision
- **25 Python Tools:** All stdlib-only, CLI-first, JSON output, zero dependencies
## See Also
- `CLAUDE.md` — full architecture diagram and integration guide
- `agent-protocol/SKILL.md` — communication standard and quality loop details
- `chief-of-staff/SKILL.md` — routing matrix for all 28 skills
FILE:CLAUDE.md
# C-Level Advisory Skills — Claude Code Guidance
A complete virtual board of directors: 28 skills covering 10 executive roles, orchestration, cross-cutting capabilities, and culture & collaboration frameworks.
## Architecture
```
/cs:setup (Founder Interview) → company-context.md
│
Chief of Staff (Router)
│
┌───────────┼───────────┐
10 Roles 6 Cross-Cut 6 Culture
│ │ │
└───────────┼────────────┘
│
Executive Mentor (Critic)
│
Decision Logger (Two-Layer Memory)
```
## Skills Overview
### C-Suite Roles (10)
| Role | Folder | Reasoning Technique | Scripts |
|------|--------|-------------------|---------|
| **CEO** | `ceo-advisor/` | Tree of Thought | strategy_analyzer, financial_scenario_analyzer |
| **CTO** | `cto-advisor/` | ReAct | tech_debt_analyzer, team_scaling_calculator |
| **COO** | `coo-advisor/` | Step by Step | ops_efficiency_analyzer, okr_tracker |
| **CPO** | `cpo-advisor/` | First Principles | pmf_scorer, portfolio_analyzer |
| **CMO** | `cmo-advisor/` | Recursion of Thought | marketing_budget_modeler, growth_model_simulator |
| **CFO** | `cfo-advisor/` | Chain of Thought | burn_rate_calculator, unit_economics_analyzer, fundraising_model |
| **CRO** | `cro-advisor/` | Chain of Thought | revenue_forecast_model, churn_analyzer |
| **CISO** | `ciso-advisor/` | Risk-Based | risk_quantifier, compliance_tracker |
| **CHRO** | `chro-advisor/` | Empathy + Data | hiring_plan_modeler, comp_benchmarker |
| **Executive Mentor** | `executive-mentor/` | Adversarial | decision_matrix_scorer, stakeholder_mapper |
### Orchestration (6)
| Skill | Folder | Purpose |
|-------|--------|---------|
| **C-Suite Onboard** | `cs-onboard/` | Founder interview → company-context.md |
| **Chief of Staff** | `chief-of-staff/` | Routes questions, triggers board meetings |
| **Board Meeting** | `board-meeting/` | 6-phase multi-agent deliberation |
| **Decision Logger** | `decision-logger/` | Two-layer memory (raw + approved) |
| **Agent Protocol** | `agent-protocol/` | Inter-agent invocation, loop prevention, quality loop |
| **Context Engine** | `context-engine/` | Company context loading + anonymization |
### Cross-Cutting Capabilities (6)
| Skill | Folder | Purpose |
|-------|--------|---------|
| **Board Deck Builder** | `board-deck-builder/` | Assembles board/investor updates |
| **Scenario War Room** | `scenario-war-room/` | Multi-variable what-if modeling |
| **Competitive Intel** | `competitive-intel/` | Systematic competitor tracking |
| **Org Health Diagnostic** | `org-health-diagnostic/` | Cross-functional health scoring |
| **M&A Playbook** | `ma-playbook/` | Acquiring or being acquired |
| **International Expansion** | `intl-expansion/` | Market entry strategy |
### Culture & Collaboration (6)
| Skill | Folder | Purpose |
|-------|--------|---------|
| **Culture Architect** | `culture-architect/` | Build and operationalize culture |
| **Company OS** | `company-os/` | EOS/Scaling Up operating system |
| **Founder Coach** | `founder-coach/` | Founder development and growth |
| **Strategic Alignment** | `strategic-alignment/` | Strategy cascade, silo detection |
| **Change Management** | `change-management/` | ADKAR-based change rollout |
| **Internal Narrative** | `internal-narrative/` | One story across all audiences |
## Executive Mentor Slash Commands
The only skill with a `plugin.json` (namespace: `em`) because it has slash commands. Other skills are invoked by name through the Chief of Staff router or directly by the user. This is intentional — only add `plugin.json` when a skill has dedicated slash commands that need a namespace.
| Command | Purpose |
|---------|---------|
| `/em:challenge` | Pre-mortem analysis of any plan |
| `/em:board-prep` | Board meeting preparation |
| `/em:hard-call` | Framework for hard decisions |
| `/em:stress-test` | Stress-test any assumption |
| `/em:postmortem` | Honest retrospective |
## Key Design Decisions
- **Two-layer memory:** Raw transcripts (reference) + approved decisions only (feeds future meetings). Prevents hallucinated consensus.
- **Phase 2 isolation:** During board meetings, agents think independently before cross-examination.
- **Internal Quality Loop:** Self-verify → peer-verify → critic pre-screen → present. No unverified output reaches the founder.
- **Proactive triggers:** Every role has context-driven early warnings that surface issues without being asked.
- **User Communication Standard:** Bottom Line → What → Why → How to Act → Your Decision. Results only, no process narration.
## Python Tools (25 total)
All scripts are stdlib-only, CLI-first, with JSON output and embedded sample data.
```bash
# Examples
python cfo-advisor/scripts/burn_rate_calculator.py
python cro-advisor/scripts/churn_analyzer.py
python cpo-advisor/scripts/pmf_scorer.py
python org-health-diagnostic/scripts/health_scorer.py
python strategic-alignment/scripts/alignment_checker.py
python decision-logger/scripts/decision_tracker.py
```
## Integration with Other Domains
| C-Level Role | Layers Above |
|-------------|-------------|
| CMO | marketing-skill/ (content, demand gen, ASO execution) |
| CFO | finance/financial-analyst (spreadsheets, DCF) |
| CRO | business-growth/ (revenue ops, sales engineering) |
| CISO | ra-qm-team/ (ISO 27001 checklists, ISMS audits) |
| CPO | product-team/ (PM toolkit, user stories, sprint planning) |
---
**Last Updated:** 2026-03-05
**Skills Deployed:** 28 skills (10 roles + 5 mentor commands + 6 orchestration + 6 cross-cutting + 6 culture)
**Python Tools:** 25 (stdlib-only)
**Reference Docs:** 52
FILE:README.md
# C-Level Advisory Skills Collection
**Complete suite of 2 executive leadership skills** covering CEO and CTO strategic decision-making and organizational leadership.
---
## 📚 Table of Contents
- [Installation](#installation)
- [Overview](#overview)
- [Skills Catalog](#skills-catalog)
- [Quick Start Guide](#quick-start-guide)
- [Common Workflows](#common-workflows)
- [Success Metrics](#success-metrics)
---
## ⚡ Installation
### Quick Install (Recommended)
Install all C-Level advisory skills with one command:
```bash
# Install all C-Level skills to all supported agents
npx ai-agent-skills install alirezarezvani/claude-skills/c-level-advisor
# Install to Claude Code only
npx ai-agent-skills install alirezarezvani/claude-skills/c-level-advisor --agent claude
# Install to Cursor only
npx ai-agent-skills install alirezarezvani/claude-skills/c-level-advisor --agent cursor
```
### Install Individual Skills
```bash
# CEO Advisor
npx ai-agent-skills install alirezarezvani/claude-skills/c-level-advisor/ceo-advisor
# CTO Advisor
npx ai-agent-skills install alirezarezvani/claude-skills/c-level-advisor/cto-advisor
```
**Supported Agents:** Claude Code, Cursor, VS Code, Copilot, Goose, Amp, Codex
**Complete Installation Guide:** See [../INSTALLATION.md](../INSTALLATION.md) for detailed instructions, troubleshooting, and manual installation.
---
## 🎯 Overview
This C-Level advisory skills collection provides executive leadership guidance for strategic decision-making, organizational development, and stakeholder management.
**What's Included:**
- **2 executive-level skills** for CEO and CTO roles
- **6 Python analysis tools** for strategy, finance, tech debt, and team scaling
- **Comprehensive frameworks** for executive decision-making, board governance, and technology leadership
- **Ready-to-use templates** for board presentations, ADRs, and strategic planning
**Ideal For:**
- CEOs and founders at startups and scale-ups
- CTOs and VP Engineering roles
- Executive leadership teams
- Board members and advisors
**Key Benefits:**
- 🎯 **Strategic clarity** with structured decision-making frameworks
- 📊 **Data-driven decisions** with financial and technical analysis tools
- 🚀 **Faster execution** with proven templates and best practices
- 💡 **Risk mitigation** through systematic evaluation processes
---
## 📦 Skills Catalog
### 1. CEO Advisor
**Status:** ✅ Production Ready | **Version:** 1.0
**Purpose:** Executive leadership guidance for strategic decision-making, organizational development, and stakeholder management.
**Key Capabilities:**
- Strategic planning and initiative evaluation
- Financial scenario modeling and business outcomes
- Executive decision framework (structured methodology)
- Leadership and organizational culture development
- Board governance and investor relations
- Stakeholder communication best practices
**Python Tools:**
- `strategy_analyzer.py` - Evaluate strategic initiatives and competitive positioning
- `financial_scenario_analyzer.py` - Model financial scenarios and business outcomes
**Core Workflows:**
1. Strategic planning and initiative evaluation
2. Financial scenario modeling
3. Board and investor communication
4. Organizational culture development
**Use When:**
- Making strategic decisions (market expansion, product pivots, fundraising)
- Preparing board presentations
- Modeling business scenarios
- Building organizational culture
- Managing stakeholder relationships
**Learn More:** [ceo-advisor/SKILL.md](ceo-advisor/SKILL.md)
---
### 2. CTO Advisor
**Status:** ✅ Production Ready | **Version:** 1.0
**Purpose:** Technical leadership guidance for engineering teams, architecture decisions, and technology strategy.
**Key Capabilities:**
- Technical debt assessment and management
- Engineering team scaling and structure planning
- Technology evaluation and selection frameworks
- Architecture decision documentation (ADRs)
- Engineering metrics (DORA metrics, velocity, quality)
- Build vs. buy analysis
**Python Tools:**
- `tech_debt_analyzer.py` - Quantify and prioritize technical debt
- `team_scaling_calculator.py` - Model engineering team growth and structure
**Core Workflows:**
1. Technical debt assessment and management
2. Engineering team scaling and structure
3. Technology evaluation and selection
4. Architecture decision documentation
**Use When:**
- Managing technical debt
- Scaling engineering teams
- Evaluating new technologies or frameworks
- Making architecture decisions
- Measuring engineering performance
**Learn More:** [cto-advisor/SKILL.md](cto-advisor/SKILL.md)
---
## 🚀 Quick Start Guide
### For CEOs
1. **Install CEO Advisor:**
```bash
npx ai-agent-skills install alirezarezvani/claude-skills/c-level-advisor/ceo-advisor
```
2. **Evaluate Strategic Initiative:**
```bash
python ceo-advisor/scripts/strategy_analyzer.py strategy-doc.md
```
3. **Model Financial Scenarios:**
```bash
python ceo-advisor/scripts/financial_scenario_analyzer.py scenarios.yaml
```
4. **Prepare for Board Meeting:**
- Use frameworks in `references/board_governance_investor_relations.md`
- Apply decision framework from `references/executive_decision_framework.md`
- Use templates from `assets/`
### For CTOs
1. **Install CTO Advisor:**
```bash
npx ai-agent-skills install alirezarezvani/claude-skills/c-level-advisor/cto-advisor
```
2. **Analyze Technical Debt:**
```bash
python cto-advisor/scripts/tech_debt_analyzer.py /path/to/codebase
```
3. **Plan Team Scaling:**
```bash
python cto-advisor/scripts/team_scaling_calculator.py --current-size 10 --target-size 50
```
4. **Document Architecture Decisions:**
- Use ADR templates from `references/architecture_decision_records.md`
- Apply technology evaluation framework
- Track engineering metrics
---
## 🔄 Common Workflows
### Workflow 1: Strategic Decision Making (CEO)
```
1. Problem Definition → CEO Advisor
- Define decision context
- Identify stakeholders
- Clarify success criteria
2. Strategic Analysis → CEO Advisor
- Strategy analyzer tool
- Competitive positioning
- Market opportunity assessment
3. Financial Modeling → CEO Advisor
- Scenario analyzer tool
- Revenue projections
- Cost-benefit analysis
4. Decision Framework → CEO Advisor
- Apply structured methodology
- Risk assessment
- Go/No-go recommendation
5. Stakeholder Communication → CEO Advisor
- Board presentation
- Investor update
- Team announcement
```
### Workflow 2: Technology Evaluation (CTO)
```
1. Technology Assessment → CTO Advisor
- Requirements gathering
- Technology landscape scan
- Evaluation criteria definition
2. Build vs. Buy Analysis → CTO Advisor
- TCO calculation
- Risk analysis
- Timeline estimation
3. Architecture Impact → CTO Advisor
- System design implications
- Integration complexity
- Migration path
4. Decision Documentation → CTO Advisor
- ADR creation
- Technical specification
- Implementation roadmap
5. Team Communication → CTO Advisor
- Engineering announcement
- Training plan
- Implementation kickoff
```
### Workflow 3: Engineering Team Scaling (CTO)
```
1. Current State Assessment → CTO Advisor
- Team structure analysis
- Velocity and quality metrics
- Bottleneck identification
2. Growth Modeling → CTO Advisor
- Team scaling calculator
- Organizational design
- Role definition
3. Hiring Plan → CTO Advisor
- Hiring timeline
- Budget requirements
- Onboarding strategy
4. Process Evolution → CTO Advisor
- Updated workflows
- Team communication
- Quality gates
5. Implementation → CTO Advisor
- Gradual rollout
- Metrics tracking
- Continuous adjustment
```
### Workflow 4: Board Preparation (CEO)
```
1. Content Preparation → CEO Advisor
- Financial summary
- Strategic updates
- Key metrics dashboard
2. Presentation Design → CEO Advisor
- Board governance frameworks
- Slide deck structure
- Data visualization
3. Q&A Preparation → CEO Advisor
- Anticipated questions
- Risk mitigation answers
- Strategic rationale
4. Rehearsal → CEO Advisor
- Timing practice
- Narrative flow
- Supporting materials
```
---
## 📊 Success Metrics
### CEO Advisor Impact
**Strategic Clarity:**
- 40% improvement in decision-making speed
- 50% reduction in strategic initiative failures
- 60% improvement in stakeholder alignment
**Financial Performance:**
- 30% better accuracy in financial projections
- 45% improvement in scenario planning effectiveness
- 25% reduction in unexpected costs
**Board & Investor Relations:**
- 50% reduction in board presentation preparation time
- 70% improvement in board feedback quality
- 40% better investor communication clarity
### CTO Advisor Impact
**Technical Debt Management:**
- 60% improvement in tech debt visibility
- 40% reduction in critical tech debt items
- 50% better resource allocation for debt reduction
**Team Scaling:**
- 45% faster time-to-productivity for new hires
- 35% reduction in team scaling mistakes
- 50% improvement in organizational design clarity
**Technology Decisions:**
- 70% reduction in technology evaluation time
- 55% improvement in build vs. buy accuracy
- 40% better architecture decision documentation
---
## 🔗 Integration with Other Teams
**CEO ↔ Product:**
- Strategic vision → Product roadmap
- Market insights → Product strategy
- Customer feedback → Product prioritization
**CEO ↔ CTO:**
- Technology strategy → Business strategy
- Engineering capacity → Business planning
- Technical decisions → Strategic initiatives
**CTO ↔ Engineering:**
- Architecture decisions → Implementation
- Tech debt priorities → Sprint planning
- Team structure → Engineering delivery
**CTO ↔ Product:**
- Technical feasibility → Product planning
- Platform capabilities → Product features
- Engineering metrics → Product velocity
---
## 📚 Additional Resources
- **CLAUDE.md:** [c-level-advisor/CLAUDE.md](CLAUDE.md) - Claude Code specific guidance (if exists)
- **Main Documentation:** [../CLAUDE.md](../CLAUDE.md)
- **Installation Guide:** [../INSTALLATION.md](../INSTALLATION.md)
---
**Last Updated:** January 2026
**Skills Deployed:** 2/2 C-Level advisory skills production-ready
**Total Tools:** 6 Python analysis tools (strategy, finance, tech debt, team scaling)
FILE:agent-protocol/SKILL.md
---
name: "agent-protocol"
description: "Inter-agent communication protocol for C-suite agent teams. Defines invocation syntax, loop prevention, isolation rules, and response formats. Use when C-suite agents need to query each other, coordinate cross-functional analysis, or run board meetings with multiple agent roles."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: agent-orchestration
updated: 2026-03-05
frameworks: invocation-patterns
---
# Inter-Agent Protocol
How C-suite agents talk to each other. Rules that prevent chaos, loops, and circular reasoning.
## Keywords
agent protocol, inter-agent communication, agent invocation, agent orchestration, multi-agent, c-suite coordination, agent chain, loop prevention, agent isolation, board meeting protocol
## Invocation Syntax
Any agent can query another using:
```
[INVOKE:role|question]
```
**Examples:**
```
[INVOKE:cfo|What's the burn rate impact of hiring 5 engineers in Q3?]
[INVOKE:cto|Can we realistically ship this feature by end of quarter?]
[INVOKE:chro|What's our typical time-to-hire for senior engineers?]
[INVOKE:cro|What does our pipeline look like for the next 90 days?]
```
**Valid roles:** `ceo`, `cfo`, `cro`, `cmo`, `cpo`, `cto`, `chro`, `coo`, `ciso`
## Response Format
Invoked agents respond using this structure:
```
[RESPONSE:role]
Key finding: [one line — the actual answer]
Supporting data:
- [data point 1]
- [data point 2]
- [data point 3 — optional]
Confidence: [high | medium | low]
Caveat: [one line — what could make this wrong]
[/RESPONSE]
```
**Example:**
```
[RESPONSE:cfo]
Key finding: Hiring 5 engineers in Q3 extends runway from 14 to 9 months at current burn.
Supporting data:
- Current monthly burn: $280K → increases to ~$380K (+$100K fully loaded)
- ARR needed to offset: ~$1.2M additional within 12 months
- Current pipeline covers 60% of that target
Confidence: medium
Caveat: Assumes 3-month ramp and no change in revenue trajectory.
[/RESPONSE]
```
## Loop Prevention (Hard Rules)
These rules are enforced unconditionally. No exceptions.
### Rule 1: No Self-Invocation
An agent cannot invoke itself.
```
❌ CFO → [INVOKE:cfo|...] — BLOCKED
```
### Rule 2: Maximum Depth = 2
Chains can go A→B→C. The third hop is blocked.
```
✅ CRO → CFO → COO (depth 2)
❌ CRO → CFO → COO → CHRO (depth 3 — BLOCKED)
```
### Rule 3: No Circular Calls
If agent A called agent B, agent B cannot call agent A in the same chain.
```
✅ CRO → CFO → CMO
❌ CRO → CFO → CRO (circular — BLOCKED)
```
### Rule 4: Chain Tracking
Each invocation carries its call chain. Format:
```
[CHAIN: cro → cfo → coo]
```
Agents check this chain before responding with another invocation.
**When blocked:** Return this instead of invoking:
```
[BLOCKED: cannot invoke cfo — circular call detected in chain cro→cfo]
State assumption used instead: [explicit assumption the agent is making]
```
## Isolation Rules
### Board Meeting Phase 2 (Independent Analysis)
**NO invocations allowed.** Each role forms independent views before cross-pollination.
- Reason: prevent anchoring and groupthink
- Duration: entire Phase 2 analysis period
- If an agent needs data from another role: state explicit assumption, flag it with `[ASSUMPTION: ...]`
### Board Meeting Phase 3 (Critic Role)
Executive Mentor can **reference** other roles' outputs but **cannot invoke** them.
- Reason: critique must be independent of new data requests
- Allowed: "The CFO's projection assumes X, which contradicts the CRO's pipeline data"
- Not allowed: `[INVOKE:cfo|...]` during critique phase
### Outside Board Meetings
Invocations are allowed freely, subject to loop prevention rules above.
## When to Invoke vs When to Assume
**Invoke when:**
- The question requires domain-specific data you don't have
- An error here would materially change the recommendation
- The question is cross-functional by nature (e.g., hiring impact on both budget and capacity)
**Assume when:**
- The data is directionally clear and precision isn't critical
- You're in Phase 2 isolation (always assume, never invoke)
- The chain is already at depth 2
- The question is minor compared to your main analysis
**When assuming, always state it:**
```
[ASSUMPTION: runway ~12 months based on typical Series A burn profile — not verified with CFO]
```
## Conflict Resolution
When two invoked agents give conflicting answers:
1. **Flag the conflict explicitly:**
```
[CONFLICT: CFO projects 14-month runway; CRO expects pipeline to close 80% → implies 18+ months]
```
2. **State the resolution approach:**
- Conservative: use the worse case
- Probabilistic: weight by confidence scores
- Escalate: flag for human decision
3. **Never silently pick one** — surface the conflict to the user.
## Broadcast Pattern (Crisis / CEO)
CEO can broadcast to all roles simultaneously:
```
[BROADCAST:all|What's the impact if we miss the fundraise?]
```
Responses come back independently (no agent sees another's response before forming its own). Aggregate after all respond.
## Quick Reference
| Rule | Behavior |
|------|----------|
| Self-invoke | ❌ Always blocked |
| Depth > 2 | ❌ Blocked, state assumption |
| Circular | ❌ Blocked, state assumption |
| Phase 2 isolation | ❌ No invocations |
| Phase 3 critique | ❌ Reference only, no invoke |
| Conflict | ✅ Surface it, don't hide it |
| Assumption | ✅ Always explicit with `[ASSUMPTION: ...]` |
## Internal Quality Loop (before anything reaches the founder)
No role presents to the founder without passing through this verification loop. The founder sees polished, verified output — not first drafts.
### Step 1: Self-Verification (every role, every time)
Before presenting, every role runs this internal checklist:
```
SELF-VERIFY CHECKLIST:
□ Source Attribution — Where did each data point come from?
✅ "ARR is $2.1M (from CRO pipeline report, Q4 actuals)"
❌ "ARR is around $2M" (no source, vague)
□ Assumption Audit — What am I assuming vs what I verified?
Tag every assumption: [VERIFIED: checked against data] or [ASSUMED: not verified]
If >50% of findings are ASSUMED → flag low confidence
□ Confidence Score — How sure am I on each finding?
🟢 High: verified data, established pattern, multiple sources
🟡 Medium: single source, reasonable inference, some uncertainty
🔴 Low: assumption-based, limited data, first-time analysis
□ Contradiction Check — Does this conflict with known context?
Check against company-context.md and recent decisions in decision-log
If it contradicts a past decision → flag explicitly
□ "So What?" Test — Does every finding have a business consequence?
If you can't answer "so what?" in one sentence → cut it
```
### Step 2: Peer Verification (cross-functional validation)
When a recommendation impacts another role's domain, that role validates BEFORE presenting.
| If your recommendation involves... | Validate with... | They check... |
|-------------------------------------|-------------------|---------------|
| Financial numbers or budget | CFO | Math, runway impact, budget reality |
| Revenue projections | CRO | Pipeline backing, historical accuracy |
| Headcount or hiring | CHRO | Market reality, comp feasibility, timeline |
| Technical feasibility or timeline | CTO | Engineering capacity, technical debt load |
| Operational process changes | COO | Capacity, dependencies, scaling impact |
| Customer-facing changes | CRO + CPO | Churn risk, product roadmap conflict |
| Security or compliance claims | CISO | Actual posture, regulation requirements |
| Market or positioning claims | CMO | Data backing, competitive reality |
**Peer validation format:**
```
[PEER-VERIFY:cfo]
Validated: ✅ Burn rate calculation correct
Adjusted: ⚠️ Hiring timeline should be Q3 not Q2 (budget constraint)
Flagged: 🔴 Missing equity cost in total comp projection
[/PEER-VERIFY]
```
**Skip peer verification when:**
- Single-domain question with no cross-functional impact
- Time-sensitive proactive alert (send alert, verify after)
- Founder explicitly asked for a quick take
### Step 3: Critic Pre-Screen (high-stakes decisions only)
For decisions that are **irreversible, high-cost, or bet-the-company**, the Executive Mentor pre-screens before the founder sees it.
**Triggers for pre-screen:**
- Involves spending > 20% of remaining runway
- Affects >30% of the team (layoffs, reorg)
- Changes company strategy or direction
- Involves external commitments (fundraising terms, partnerships, M&A)
- Any recommendation where all roles agree (suspicious consensus)
**Pre-screen output:**
```
[CRITIC-SCREEN]
Weakest point: [The single biggest vulnerability in this recommendation]
Missing perspective: [What nobody considered]
If wrong, the cost is: [Quantified downside]
Proceed: ✅ With noted risks | ⚠️ After addressing [specific gap] | 🔴 Rethink
[/CRITIC-SCREEN]
```
### Step 4: Course Correction (after founder feedback)
The loop doesn't end at delivery. After the founder responds:
```
FOUNDER FEEDBACK LOOP:
1. Founder approves → log decision (Layer 2), assign actions
2. Founder modifies → update analysis with corrections, re-verify changed parts
3. Founder rejects → log rejection with DO_NOT_RESURFACE, understand WHY
4. Founder asks follow-up → deepen analysis on specific point, re-verify
POST-DECISION REVIEW (30/60/90 days):
- Was the recommendation correct?
- What did we miss?
- Update company-context.md with what we learned
- If wrong → document the lesson, adjust future analysis
```
### Verification Level by Stakes
| Stakes | Self-Verify | Peer-Verify | Critic Pre-Screen |
|--------|-------------|-------------|-------------------|
| Low (informational) | ✅ Required | ❌ Skip | ❌ Skip |
| Medium (operational) | ✅ Required | ✅ Required | ❌ Skip |
| High (strategic) | ✅ Required | ✅ Required | ✅ Required |
| Critical (irreversible) | ✅ Required | ✅ Required | ✅ Required + board meeting |
### What Changes in the Output Format
The verified output adds confidence and source information:
```
BOTTOM LINE
[Answer] — Confidence: 🟢 High
WHAT
• [Finding 1] [VERIFIED: Q4 actuals] 🟢
• [Finding 2] [VERIFIED: CRO pipeline data] 🟢
• [Finding 3] [ASSUMED: based on industry benchmarks] 🟡
PEER-VERIFIED BY: CFO (math ✅), CTO (timeline ⚠️ adjusted to Q3)
```
---
## User Communication Standard
All C-suite output to the founder follows ONE format. No exceptions. The founder is the decision-maker — give them results, not process.
### Standard Output (single-role response)
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 [ROLE] — [Topic]
BOTTOM LINE
[One sentence. The answer. No preamble.]
WHAT
• [Finding 1 — most critical]
• [Finding 2]
• [Finding 3]
(Max 5 bullets. If more needed → reference doc.)
WHY THIS MATTERS
[1-2 sentences. Business impact. Not theory — consequence.]
HOW TO ACT
1. [Action] → [Owner] → [Deadline]
2. [Action] → [Owner] → [Deadline]
3. [Action] → [Owner] → [Deadline]
⚠️ RISKS (if any)
• [Risk + what triggers it]
🔑 YOUR DECISION (if needed)
Option A: [Description] — [Trade-off]
Option B: [Description] — [Trade-off]
Recommendation: [Which and why, in one line]
📎 DETAIL: [reference doc or script output for deep-dive]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### Proactive Alert (unsolicited — triggered by context)
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🚩 [ROLE] — Proactive Alert
WHAT I NOTICED
[What triggered this — specific, not vague]
WHY IT MATTERS
[Business consequence if ignored — in dollars, time, or risk]
RECOMMENDED ACTION
[Exactly what to do, who does it, by when]
URGENCY: 🔴 Act today | 🟡 This week | ⚪ Next review
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### Board Meeting Output (multi-role synthesis)
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 BOARD MEETING — [Date] — [Agenda Topic]
DECISION REQUIRED
[Frame the decision in one sentence]
PERSPECTIVES
CEO: [one-line position]
CFO: [one-line position]
CRO: [one-line position]
[... only roles that contributed]
WHERE THEY AGREE
• [Consensus point 1]
• [Consensus point 2]
WHERE THEY DISAGREE
• [Conflict] — CEO says X, CFO says Y
• [Conflict] — CRO says X, CPO says Y
CRITIC'S VIEW (Executive Mentor)
[The uncomfortable truth nobody else said]
RECOMMENDED DECISION
[Clear recommendation with rationale]
ACTION ITEMS
1. [Action] → [Owner] → [Deadline]
2. [Action] → [Owner] → [Deadline]
3. [Action] → [Owner] → [Deadline]
🔑 YOUR CALL
[Options if you disagree with the recommendation]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### Communication Rules (non-negotiable)
1. **Bottom line first.** Always. The founder's time is the scarcest resource.
2. **Results and decisions only.** No process narration ("First I analyzed..."). No thinking out loud.
3. **What + Why + How.** Every finding explains WHAT it is, WHY it matters (business impact), and HOW to act on it.
4. **Max 5 bullets per section.** Longer = reference doc.
5. **Actions have owners and deadlines.** "We should consider" is banned. Who does what by when.
6. **Decisions framed as options.** Not "what do you think?" — "Option A or B, here's the trade-off, here's my recommendation."
7. **The founder decides.** Roles recommend. The founder approves, modifies, or rejects. Every output respects this hierarchy.
8. **Risks are concrete.** Not "there might be risks" — "if X happens, Y breaks, costing $Z."
9. **No jargon without explanation.** If you use a term, explain it on first use.
10. **Silence is an option.** If there's nothing to report, don't fabricate updates.
## Reference
- `references/invocation-patterns.md` — common cross-functional patterns with examples
FILE:agent-protocol/references/invocation-patterns.md
# Invocation Patterns — Inter-Agent Protocol
Common cross-functional patterns, with examples and guidance on when to invoke vs assume.
---
## Pattern 1: Revenue Planning Chain
**CRO → CFO → CMO**
Used when: setting revenue targets, allocating growth budget, modeling pipeline scenarios.
```
CRO initiates:
"Planning Q3 revenue targets. Need financial constraints and marketing input."
CRO → [INVOKE:cfo|What's our Q3 revenue target given current runway and burn?]
CFO responds:
[RESPONSE:cfo]
Key finding: To maintain 12-month runway without new raise, Q3 ARR target ≥ $2.4M (+20% QoQ)
Supporting data:
- Current ARR: $2.0M, burn: $180K/month, runway: 14 months
- 20% QoQ growth maintains runway; <15% triggers bridge discussion
- Board expectation communicated as 25% QoQ
Confidence: high
Caveat: Assumes no new hires or major infrastructure spend in Q3.
[/RESPONSE]
CRO → [INVOKE:cmo|Given a $2.4M Q3 ARR target, what pipeline coverage and CAC should we plan for?]
[CHAIN: cro → cfo → cmo]
CMO responds:
[RESPONSE:cmo]
Key finding: Need 3x pipeline ($7.2M) and CAC ≤ $8,400 to hit $2.4M ARR target.
Supporting data:
- Historical close rate: 33% → 3x coverage required
- Current CAC: $9,100 → 8% efficiency improvement needed
- Recommended channel mix: 60% outbound, 30% inbound, 10% partner
Confidence: medium
Caveat: Close rate assumes no sales team changes; any rep turnover drops it to 25%.
[/RESPONSE]
```
**What CRO does with this:** Synthesizes CFO constraint (target) + CMO playbook (how to hit it) into a revenue plan. Does not invoke further (depth limit reached).
---
## Pattern 2: Hiring Decision Chain
**CHRO → CFO → COO**
Used when: headcount planning, role approval, capacity vs budget tradeoffs.
```
CHRO initiates:
"Engineering wants to hire 3 senior engineers. Need budget clearance and capacity validation."
CHRO → [INVOKE:cfo|Can we afford 3 senior engineers in H2? Fully loaded ~$180K each.]
[RESPONSE:cfo]
Key finding: Budget supports 2 hires now; 3rd is contingent on Q2 ARR closing above $2.1M.
Supporting data:
- Current H2 headcount budget: $340K remaining
- 2 hires = $270K fully loaded → $70K buffer
- 3 hires = $405K → $65K over; triggers board approval
Confidence: high
Caveat: Based on current runway projection — fundraise close changes this completely.
[/RESPONSE]
CHRO → [INVOKE:coo|Do operations support onboarding 2-3 engineers in H2? Any capacity constraints?]
[CHAIN: chro → cfo → coo]
[RESPONSE:coo]
Key finding: Can absorb 2 comfortably; 3rd requires dedicated onboarding lead or delays velocity 6 weeks.
Supporting data:
- Current eng team: 8 → adding 2 is 25% growth (manageable)
- Onboarding bottleneck: 1 senior eng mentor per 1-2 new hires, currently at capacity
- Dev environment setup: 3-week lead time per hire
Confidence: medium
Caveat: Assumes no concurrent product launches requiring full team focus.
[/RESPONSE]
CHRO synthesis: Recommends 2 hires now, pipeline 3rd for Q3 contingent on ARR milestone.
```
---
## Pattern 3: Product Launch Chain
**CPO → CTO → CMO**
Used when: planning feature launches, assessing readiness, aligning go-to-market.
```
CPO initiates:
"Planning Q3 launch of [Feature X]. Need technical readiness and GTM input."
CPO → [INVOKE:cto|Is Feature X technically ready for Q3 launch? Key risks?]
[RESPONSE:cto]
Key finding: 70% complete; Q3 launch is achievable with scope cut — 2 items must move to Q4.
Supporting data:
- Core feature: complete; performance optimization: 3 weeks remaining
- Infrastructure scaling: needs load testing at 10x current volume
- Scope cuts needed: advanced analytics dashboard, third-party integrations
Confidence: medium
Caveat: Assumes no critical bugs found in load testing; historical rate suggests 1-2 found.
[/RESPONSE]
CPO → [INVOKE:cmo|Given Q3 launch of Feature X (core only, no analytics/integrations), what's the GTM plan?]
[CHAIN: cpo → cto → cmo]
[RESPONSE:cmo]
Key finding: Core-only launch works for existing customers; new logo acquisition needs integrations — recommend phased announcement.
Supporting data:
- Phase 1: Early access for top 20 accounts → testimonials and case studies
- Phase 2: Full launch with integration partners in Q4
- CAC impact: core-only launch reduces new logo conversion rate 15%
Confidence: medium
Caveat: Competitor launched similar feature last month — window for differentiation is narrow.
[/RESPONSE]
```
---
## Pattern 4: Crisis Broadcast
**CEO → All Roles**
Used when: major adverse event, fundraise miss, key customer loss, market disruption.
```
CEO broadcasts:
[BROADCAST:all|Scenario: largest customer (28% of ARR) gives 60-day termination notice.
Each role: what's the immediate impact on your domain and your top recommended action?]
Roles respond INDEPENDENTLY (no agent sees others' responses first):
[RESPONSE:cfo]
Key finding: 28% ARR loss drops runway from 14 to 8 months at current burn.
Supporting data:
- Lost ARR: ~$560K → burn multiple increases from 1.8x to 3.2x
- Bridge options: $500K credit line (pre-approved), defer 2 planned hires
- Fundraise timeline must compress: target term sheet within 60 days
Confidence: high
Caveat: Assumes no secondary customer churn triggered by news.
[/RESPONSE]
[RESPONSE:cro]
Key finding: Pipeline must replace $560K ARR within 6 months to avoid emergency measures.
Supporting data:
- Current qualified pipeline: $1.8M → need 31% close rate (currently 28%)
- 3 deals in late stage can cover 70% of gap if closed this quarter
- Churn risk: 2 other accounts had similar usage patterns — immediate EBR required
Confidence: medium
Caveat: Close rate may drop if word spreads; manage communication carefully.
[/RESPONSE]
[RESPONSE:chro]
Key finding: Hiring freeze recommended immediately; retain all current staff.
Supporting data:
- 4 open reqs should pause pending CFO guidance
- At-risk of counter-poaching: 2 senior engineers, 1 product lead
- Retention bonuses for key staff: ~$80K to secure 12-month commitments
Confidence: high
Caveat: Morale impact of freeze can accelerate attrition if not communicated well.
[/RESPONSE]
[RESPONSE:cpo]
Key finding: Accelerate features that differentiate with remaining accounts; reduce scope to core.
Supporting data:
- Lost customer's feature requests: deprioritize (no longer strategic)
- Features most valued by remaining top 5 accounts: [list from account data]
- Reduce Q3 scope by 30% to free engineering capacity for retention work
Confidence: medium
Caveat: Need sales input on which features are blocking new logo deals.
[/RESPONSE]
[RESPONSE:cmo]
Key finding: Control the narrative; proactive communication to market reduces churn cascade risk.
Supporting data:
- Draft customer communication within 48 hours (frame as strategic pivot, not loss)
- Press: no comment strategy unless proactively asked
- Replace pipeline: double down on ICP segments where we're strongest
Confidence: medium
Caveat: If customer goes public with criticism, narrative control becomes much harder.
[/RESPONSE]
CEO synthesis: [Aggregates all 9 responses, identifies conflicts, sets priorities]
```
---
## When to Invoke vs When to Assume
### Invoke when:
- Cross-functional data is material to the decision
- Getting it wrong changes the recommendation significantly
- The other role has data you genuinely don't have
- Time allows (not in Phase 2 isolation)
### Assume when:
- You're in Phase 2 (always — no exceptions)
- The chain is at depth 2 (you cannot invoke further)
- The answer is directionally obvious (e.g., "CFO will care about runway")
- The precision doesn't change the recommendation
### State assumptions explicitly:
```
[ASSUMPTION: runway ~12 months — not verified with CFO; actual may vary ±20%]
[ASSUMPTION: CAC ~$8K based on industry benchmark — CMO has actual figures]
[ASSUMPTION: engineering capacity at ~70% — not verified with CTO]
```
---
## Handling Conflicting Responses
When two agents give incompatible answers, surface it:
```
[CONFLICT DETECTED]
CFO says: runway extends to 18 months if Q3 targets hit
CRO says: only 45% confidence Q3 targets will be hit
Resolution: use probabilistic blend
- 45% probability: 18-month runway (optimistic case)
- 55% probability: 11-month runway (current trajectory)
Expected value: ~14 months
Recommendation: plan for 12 months, trigger bridge at 10.
[/CONFLICT]
```
**Resolution options:**
1. **Conservative:** Use worse case — appropriate for cash/runway decisions
2. **Probabilistic:** Weight by confidence scores — appropriate for planning
3. **Escalate:** Flag for human decision — appropriate for high-stakes irreversible choices
4. **Time-box:** Gather more data within 48 hours — appropriate when data gap is closeable
---
## Anti-Patterns to Avoid
| Anti-pattern | Problem | Fix |
|---|---|---|
| Invoke to validate your own conclusion | Confirmation bias loop | Ask open-ended questions |
| Invoke when assuming works | Unnecessary latency | State assumption clearly |
| Hide conflicts between responses | Bad synthesis | Always surface conflicts |
| Invoke across depth > 2 | Loop risk | State assumption at depth 2 |
| Invoke during Phase 2 | Groupthink contamination | Flag with [ASSUMPTION:] |
| Vague questions | Poor responses | Specific, scoped questions only |
FILE:board-deck-builder/SKILL.md
---
name: "board-deck-builder"
description: "Assembles comprehensive board and investor update decks by pulling perspectives from all C-suite roles. Use when preparing board meetings, investor updates, quarterly business reviews, or fundraising narratives. Covers structure, narrative framework, bad news delivery, and common mistakes."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: board-governance
updated: 2026-03-05
frameworks: deck-frameworks, board-deck-template
---
# Board Deck Builder
Build board decks that tell a story — not just show data. Every section has an owner, a narrative, and a "so what."
## Keywords
board deck, investor update, board meeting, board pack, investor relations, quarterly review, board presentation, fundraising deck, investor deck, board narrative, QBR, quarterly business review
## Quick Start
```
/board-deck [quarterly|monthly|fundraising] [stage: seed|seriesA|seriesB]
```
Provide available metrics. The builder fills gaps with explicit placeholders — never invents numbers.
## Deck Structure (Standard Order)
Every section follows: **Headline → Data → Narrative → Ask/Next**
### 1. Executive Summary (CEO)
**3 sentences. No more.**
- Sentence 1: State of the business (where we are)
- Sentence 2: Biggest thing that happened this period
- Sentence 3: Where we're going next quarter
*Bad:* "We had a good quarter with lots of progress across all areas."
*Good:* "We closed Q3 at $2.4M ARR (+22% QoQ), signed our largest enterprise contract, and enter Q4 with 14-month runway. The strategic shift to mid-market is working — ACV up 40% and sales cycle down 3 weeks. Q4 priority: close the $3M Series A and hit $2.8M ARR."
### 2. Key Metrics Dashboard (COO)
**6-8 metrics max. Use a table.**
| Metric | This Period | Last Period | Target | Status |
|--------|-------------|-------------|--------|--------|
| ARR | $2.4M | $1.97M | $2.3M | ✅ |
| MoM growth | 8.1% | 7.2% | 7.5% | ✅ |
| Burn multiple | 1.8x | 2.1x | <2x | ✅ |
| NRR | 112% | 108% | >110% | ✅ |
| CAC payback | 11 months | 14 months | <12 months | ✅ |
| Headcount | 24 | 21 | 25 | 🟡 |
Pick metrics the board actually tracks. Swap out anything they've said they don't care about.
### 3. Financial Update (CFO)
- P&L summary: Revenue, COGS, Gross margin, OpEx, Net burn
- Cash position and runway (months)
- Burn multiple trend (3-quarter view)
- Variance to plan (what was different and why)
- Forecast update for next quarter
**One sentence on each variance.** Boards hate "revenue was below target" with no explanation. Say why.
### 4. Revenue & Pipeline (CRO)
- ARR waterfall: starting → new → expansion → churn → ending
- NRR and logo churn rates
- Pipeline by stage (in $, not just count)
- Forecast: next quarter with confidence level
- Top 3 deals: name/amount/close date/risk
**The forecast must have a confidence level.** "We expect $2.8M" is weak. "High confidence $2.6M, upside to $2.9M if two late-stage deals close" is useful.
### 5. Product Update (CPO)
- Shipped this quarter: 3-5 bullets, user impact for each
- Shipping next quarter: 3-5 bullets with target dates
- PMF signal: NPS trend, DAU/MAU ratio, feature adoption
- One key learning from customer research
**No feature lists.** Only features with evidence of user impact.
### 6. Growth & Marketing (CMO)
- CAC by channel (table)
- Pipeline contribution by channel ($)
- Brand/awareness metrics relevant to stage (traffic, share of voice)
- What's working, what's being cut, what's being tested
### 7. Engineering & Technical (CTO)
- Delivery velocity trend (last 4 quarters)
- Tech debt ratio and plan
- Infrastructure: uptime, incidents, cost trend
- Security posture (one line, flag anything pending)
**Keep this short unless there's a material issue.** Boards don't need sprint details.
### 8. Team & People (CHRO)
- Headcount: actual vs plan
- Hiring: offers out, pipeline, time-to-fill trend
- Attrition: regrettable vs non-regrettable
- Engagement: last survey score, trend
- Key hires this quarter, key open roles
### 9. Risk & Security (CISO)
- Security posture: status of critical controls
- Compliance: certifications in progress, deadlines
- Incidents this quarter (if any): impact, resolution, prevention
- Top 3 risks and mitigation status
### 10. Strategic Outlook (CEO)
- Next quarter priorities: 3-5 items, ranked
- Key decisions needed from the board
- Asks: budget, introductions, advice, votes
**The "asks" slide is the most important.** Be specific. "We'd like 3 warm introductions to CFOs at Series B companies" beats "any help would be appreciated."
### 11. Appendix
- Detailed financial model
- Full pipeline data
- Cohort retention charts
- Customer case studies
- Detailed headcount breakdown
---
## Narrative Framework
Boards see 10+ decks per quarter. Yours needs a through-line.
**The 4-Act Structure:**
1. **Where we said we'd be** (last quarter's targets)
2. **Where we actually are** (honest assessment)
3. **Why the gap exists** (one cause per variance, not excuses)
4. **What we're doing about it** (specific, dated actions)
This works for good news AND bad news. It's credible because it acknowledges reality.
**Opening frame:** Start with the one thing that matters most — the board should know the key message by slide 3, not slide 30.
---
## Delivering Bad News
Never bury it. Boards find out eventually. Finding out late makes it worse.
**Framework:**
1. **State it plainly** — "We missed Q3 ARR target by $300K (12% gap)"
2. **Own the cause** — "Primary driver was longer-than-expected sales cycle in enterprise segment"
3. **Show you understand it** — "We analyzed 8 lost/stalled deals; the pattern is X"
4. **Present the fix** — "We've made 3 changes: [specific, dated changes]"
5. **Update the forecast** — "Revised Q4 target is $2.6M; here's the bottom-up build"
**What NOT to do:**
- Don't lead with good news to soften bad news — boards notice and distrust the framing
- Don't explain without owning — "market conditions" is not a cause, it's a context
- Don't present a fix without data behind it
- Don't show a revised forecast without showing your assumptions
---
## Common Board Deck Mistakes
| Mistake | Fix |
|---------|-----|
| Too many slides (>25) | Cut ruthlessly — if you can't explain it in the room, the slide is wrong |
| Metrics without targets | Every metric needs a target and a status |
| No narrative | Data without story forces boards to draw their own conclusions |
| Burying bad news | Lead with it, own it, fix it |
| Vague asks | Specific, actionable, person-assigned asks only |
| No variance explanation | Every gap from target needs one-sentence cause |
| Stale appendix | Appendix is only useful if it's current |
| Designing for the reader, not the room | Decks are presented — they must work spoken aloud |
---
## Cadence Notes
**Quarterly (standard):** Full deck, all sections, 20-30 slides. Sent 48 hours in advance.
**Monthly (for early-stage):** Condensed — metrics dashboard, financials, pipeline, top risks. 8-12 slides.
**Fundraising:** Opens with market/vision, closes with ask. See `references/deck-frameworks.md` for Sequoia format.
## References
- `references/deck-frameworks.md` — SaaS board pack format, Sequoia structure, investor tailoring
- `templates/board-deck-template.md` — fill-in template for complete board decks
FILE:board-deck-builder/references/deck-frameworks.md
# Board Deck Frameworks
## The SaaS Board Pack (Christoph Janz / Point Nine Style)
Point Nine's board pack format became the de facto standard for early-stage SaaS. Core principle: **the numbers tell the story; the narrative explains the numbers.**
### Required Metrics (non-negotiable for SaaS boards)
- **ARR** (not MRR — boards think annually)
- **MoM / QoQ growth rate**
- **NRR (Net Revenue Retention)** — the single most important SaaS metric
- **Gross margin** — typically 60-80% SaaS; <60% is a flag
- **CAC payback period** — months to recover customer acquisition cost
- **Burn multiple** = net burn / net new ARR; <2x is good, >3x is a problem
- **Runway** — months at current burn
### Point Nine Benchmark Targets (Series A SaaS)
| Metric | Good | Great | Warning |
|--------|------|-------|---------|
| MoM growth | 10-15% | >20% | <7% |
| NRR | >110% | >130% | <100% |
| Gross margin | >65% | >75% | <60% |
| CAC payback | <18 months | <12 months | >24 months |
| Burn multiple | <2x | <1.5x | >3x |
| Logo churn | <10%/yr | <5%/yr | >15%/yr |
### SaaS ARR Waterfall (Christoph Janz Format)
Show this every quarter:
```
Starting ARR: $1,970,000
+ New ARR: +$480,000 (new logos)
+ Expansion ARR: +$120,000 (upsells/cross-sells)
- Churned ARR: -$90,000 (cancellations)
- Contraction ARR: -$35,000 (downgrades)
= Ending ARR: $2,445,000
```
NRR = (Ending - New) / Starting = ($1,965K) / ($1,970K) = 99.7% ← flag this
---
## Sequoia Board Deck Structure
Sequoia's canonical deck (used for both fundraising and board updates):
1. **Company Purpose** — one sentence, the existential "why"
2. **The Problem** — pain, size, who has it
3. **The Solution** — what you do, how it's different
4. **Why Now** — market timing, tailwinds, enabling factors
5. **Market Size** — TAM/SAM/SOM with methodology
6. **Business Model** — how you make money
7. **Traction** — proof it's working (growth, retention, logos)
8. **Team** — why you're the ones to win this
9. **Financials** — 3-year model, current metrics
10. **The Ask** — amount, use of funds, milestones to next round
**For ongoing board updates:** Swap 1-5 (context) for "State of the Business" and "Last Quarter vs Plan." Boards know the company — skip the pitch.
---
## Investor-Specific Tailoring
### What Different Investor Types Care About
**Early-stage VCs (Seed, A):**
- Growth rate above all else
- NRR — "does the product retain?"
- Founder-market fit narrative
- Milestone achievement vs last board meeting
**Growth-stage VCs (B, C):**
- Capital efficiency (burn multiple, CAC payback)
- GTM repeatability — can you hire 10 AEs and have it work?
- Market leadership signals
- Path to profitability (even if years away)
**Strategic investors:**
- Synergies with their portfolio/business
- Technology differentiation
- Partnership potential
**Angels:**
- Team above all
- Personal conviction in the thesis
- Exit scenarios
### Tailoring the Narrative
- If you're ahead of plan: "Here's why, and here's how we'll sustain it"
- If you're behind plan: "Here's why, here's what we've learned, here's the new plan"
- If the plan was wrong: "The assumption that was wrong, what we know now, updated thesis"
Never pretend the plan was right when it wasn't. Board members have memories and models.
---
## How to Present Bad News
Boards have seen everything. What loses credibility isn't bad results — it's bad framing.
### The Credibility Formula
1. **Lead with the headline** — "We missed ARR target by 18%"
2. **Quantify the gap** — absolute and percentage
3. **Diagnose the cause** (one primary, max two secondary)
4. **Show your work** — "We analyzed 12 churned/stalled deals and found..."
5. **Present the fix** — specific, dated, owned by a name
6. **Update the forecast** — bottom-up rebuild, not wishful thinking
7. **Flag the risk** — "If X doesn't close, here's the contingency"
### What "Showing Your Work" Looks Like
Bad: "Sales cycle was longer than expected."
Good: "Sales cycle stretched from 45 to 72 days. Root cause: new legal review requirement at enterprise accounts, triggered by our SOC 2 Type II gap. Fix: SOC 2 audit underway (target: Dec 15), and we've pre-built contract language to accelerate review. Impact: estimated 3 stalled deals ($420K ARR) unblock in Q4."
### Scenarios and How to Handle Each
| Scenario | Frame |
|----------|-------|
| Missed revenue target | Lead with it; diagnose cause; bottom-up revised forecast |
| Key customer churned | Announce it; explain why; show retention analysis of remaining accounts |
| Key exec left | Announce it; show succession/coverage plan; don't overpromise the replacement timeline |
| Burn accelerated | Show P&L detail; explain what drove it; adjust runway projection; plan to fix |
| Market headwinds | Acknowledge; show relative performance vs peers; pivot if needed |
| Fundraise delayed | Runway impact; bridge options; revised timeline |
---
## Appendix Data That Boards Actually Use
Boards use the appendix for due diligence, not during the meeting. Include:
**Financial:**
- Full P&L (monthly for last 4 quarters)
- Cash flow statement
- 3-year model with assumptions
- Unit economics by cohort
**Revenue:**
- Customer list by ARR (anonymized or full, per board agreement)
- Pipeline detail by deal
- Cohort analysis (NRR by cohort vintage)
- Churn analysis: when, why, segment
**Product:**
- Feature adoption rates
- NPS score distribution and trend
- DAU/MAU by segment
**Team:**
- Org chart
- Full headcount list with fully loaded costs
- Open reqs with priority ranking
**One rule:** If the appendix is more than 20 slides, you have too much. Boards won't read it.
---
## Quarterly vs Monthly Board Meetings
### Quarterly (Series A+)
- Full board pack, all sections
- 2 hours: 30 min pre-read, 90 min discussion
- Voting items at end
- Sent 48 hours before (72 hours preferred)
- Add 1-2 "deep dive" topics beyond standard update
### Monthly (Seed / High-Growth A)
- Metrics dashboard + financials + top risks only
- 45-60 minutes
- Informal tone, more conversational
- Sent 24 hours before
- Skip slides for items where nothing changed
### When to Increase Frequency
- Approaching 6-month runway
- Major strategic pivot
- Fundraise in progress
- Significant underperformance vs plan
- M&A discussions
---
## Meeting Logistics (Often Overlooked)
- **Pre-read requirement:** Board packs should be read before the meeting. If you're presenting slides, you're wasting time.
- **Discussion format:** "I'll be brief on X since you've read it. Want to spend time on Y?" — respect board members' time
- **One note-taker:** CEO's EA or COO; not the CEO (they need to be present)
- **Follow-up within 24 hours:** Action items, voting outcomes, next meeting date
- **Board portal vs email:** Use a board portal (Carta, Boardable, Notion) for version control and D&O protection
FILE:board-deck-builder/templates/board-deck-template.md
# Board Deck Template
Fill in bracketed fields. Remove placeholders before sharing. Never invent numbers — use `[TBD]` if unknown.
---
## Slide 1: Executive Summary (CEO)
**[Company Name] — Q[X] [Year] Board Update**
> [One sentence: State of the business — where you are.]
> [One sentence: The most important thing that happened this quarter.]
> [One sentence: Where you're going next quarter and what determines success.]
---
## Slide 2: Key Metrics Dashboard (COO)
**Quarter at a Glance**
| Metric | Q[X] Actual | Q[X] Target | Q[X-1] Actual | Status |
|--------|-------------|-------------|---------------|--------|
| ARR | $[X]M | $[X]M | $[X]M | [✅/🟡/🔴] |
| QoQ Growth | [X]% | [X]% | [X]% | [✅/🟡/🔴] |
| NRR | [X]% | >[X]% | [X]% | [✅/🟡/🔴] |
| Gross Margin | [X]% | >[X]% | [X]% | [✅/🟡/🔴] |
| Burn Multiple | [X]x | <[X]x | [X]x | [✅/🟡/🔴] |
| Runway | [X] months | >[X] months | [X] months | [✅/🟡/🔴] |
| Headcount | [X] | [X] | [X] | [✅/🟡/🔴] |
| CAC Payback | [X] months | <[X] months | [X] months | [✅/🟡/🔴] |
---
## Slide 3: Financial Update (CFO)
**P&L Summary**
| | Q[X] | Q[X-1] | QoQ |
|--|------|--------|-----|
| Revenue | $[X]K | $[X]K | [+/-X]% |
| COGS | $[X]K | $[X]K | |
| Gross Profit | $[X]K | $[X]K | |
| Gross Margin | [X]% | [X]% | |
| OpEx | $[X]K | $[X]K | |
| Net Burn | $[X]K | $[X]K | |
**Cash & Runway**
- Cash on hand: $[X]M
- Monthly burn: $[X]K
- Runway: [X] months
- Burn multiple: [X]x (target: <2x)
**Variance to Plan**
- Revenue: [+/-$X]K vs plan — [one sentence cause]
- Burn: [+/-$X]K vs plan — [one sentence cause]
**Q[X+1] Forecast:** $[X]M revenue, $[X]K burn — [confidence: high/medium/low]
---
## Slide 4: Revenue & Pipeline (CRO)
**ARR Waterfall**
```
Starting ARR: $[X]M
+ New ARR: +$[X]K
+ Expansion ARR: +$[X]K
- Churned ARR: -$[X]K
- Contraction ARR: -$[X]K
= Ending ARR: $[X]M
```
**Health Metrics**
- NRR: [X]% | Logo churn: [X]% | Avg ACV: $[X]K
**Pipeline (next 90 days)**
| Stage | # Deals | $ Value |
|-------|---------|---------|
| Proposal | [X] | $[X]K |
| Negotiation | [X] | $[X]K |
| Verbal commit | [X] | $[X]K |
**Q[X+1] Forecast:** $[X]M ARR — [one sentence confidence statement]
**Top 3 Deals**
1. [Company] — $[X]K ARR — close date [X] — risk: [one word]
2. [Company] — $[X]K ARR — close date [X] — risk: [one word]
3. [Company] — $[X]K ARR — close date [X] — risk: [one word]
---
## Slide 5: Product Update (CPO)
**Shipped This Quarter**
- [Feature/initiative] — impact: [metric or user outcome]
- [Feature/initiative] — impact: [metric or user outcome]
- [Feature/initiative] — impact: [metric or user outcome]
**Shipping Next Quarter**
- [Feature] — target: [date] — why it matters: [one line]
- [Feature] — target: [date] — why it matters: [one line]
- [Feature] — target: [date] — why it matters: [one line]
**PMF Signals**
- NPS: [X] (trend: [up/flat/down])
- DAU/MAU: [X]%
- Feature adoption ([key feature]): [X]%
**Key Learning:** [One thing customer research taught you this quarter]
---
## Slide 6: Growth & Marketing (CMO)
**CAC by Channel**
| Channel | CAC | Pipeline $ | % of Total |
|---------|-----|-----------|------------|
| Outbound | $[X]K | $[X]K | [X]% |
| Inbound | $[X]K | $[X]K | [X]% |
| Partner | $[X]K | $[X]K | [X]% |
**What's Working:** [One channel or initiative with data]
**What We Cut:** [One thing, and why]
**What We're Testing:** [One experiment running now]
---
## Slide 7: Engineering & Technical (CTO)
**Delivery**
- Velocity trend: [up/flat/down vs last quarter]
- Q[X] commitments delivered: [X]% on time
**Quality & Reliability**
- P0/P1 incidents: [X] (vs [X] last quarter)
- Uptime: [X]%
- Infrastructure cost: $[X]K/month (trend: [up/flat/down])
**Tech Debt**
- Ratio: [X]% of roadmap allocated to debt reduction
- Key item in progress: [description, target date]
**Security:** [one line status; flag anything pending]
---
## Slide 8: Team & People (CHRO)
**Headcount**
- Total: [X] (vs [X] plan, [X] last quarter)
- By function: Eng [X], Product [X], Sales [X], CS [X], G&A [X]
**Hiring**
- Hired this quarter: [X]
- Open reqs: [X] — time-to-fill avg: [X] days
- Offers outstanding: [X]
**Retention**
- Regrettable attrition: [X]% (annualized)
- Engagement score: [X]/10 (trend: [up/flat/down])
**Notable Hires:** [Name, role — one sentence on why they matter]
**Key Open Roles:** [Role, priority: critical/high/medium]
---
## Slide 9: Risk & Security (CISO)
**Compliance Status**
| Certification | Status | Target Date |
|--------------|--------|-------------|
| [SOC 2 / ISO 27001 / etc.] | [In progress / Complete / Not started] | [Date] |
**Security Posture:** [One line — overall status]
**Incidents This Quarter:** [X] total — [description if >0]
**Top Risks**
1. [Risk] — likelihood: [H/M/L] — impact: [H/M/L] — mitigation: [one line]
2. [Risk] — likelihood: [H/M/L] — impact: [H/M/L] — mitigation: [one line]
3. [Risk] — likelihood: [H/M/L] — impact: [H/M/L] — mitigation: [one line]
---
## Slide 10: Strategic Outlook (CEO)
**Q[X+1] Priorities**
1. [Priority] — owner: [name] — success metric: [specific]
2. [Priority] — owner: [name] — success metric: [specific]
3. [Priority] — owner: [name] — success metric: [specific]
**Asks from the Board**
- [Specific ask: warm intro / advice / vote / resource]
- [Specific ask]
- [Specific ask]
**Decisions Needed Today**
- [Decision with options]: [Option A] vs [Option B] — recommendation: [A/B] — rationale: [one line]
---
## Appendix
- A1: Full P&L (monthly, last 4 quarters)
- A2: 3-year financial model
- A3: Customer list / ARR breakdown
- A4: Full pipeline by deal
- A5: Cohort retention analysis
- A6: Org chart + headcount detail
- A7: [Other as relevant]
FILE:board-meeting/SKILL.md
---
name: "board-meeting"
description: "Multi-agent board meeting protocol for strategic decisions. Runs a structured 6-phase deliberation: context loading, independent C-suite contributions (isolated, no cross-pollination), critic analysis, synthesis, founder review, and decision extraction. Use when the user invokes /cs:board, calls a board meeting, or wants structured multi-perspective executive deliberation on a strategic question."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: board-protocol
updated: 2026-03-05
frameworks: 6-phase-board, two-layer-memory, independent-contributions
---
# Board Meeting Protocol
Structured multi-agent deliberation that prevents groupthink, captures minority views, and produces clean, actionable decisions.
## Keywords
board meeting, executive deliberation, strategic decision, C-suite, multi-agent, /cs:board, founder review, decision extraction, independent perspectives
## Invoke
`/cs:board [topic]` — e.g. `/cs:board Should we expand to Spain in Q3?`
---
## The 6-Phase Protocol
### PHASE 1: Context Gathering
1. Load `memory/company-context.md`
2. Load `memory/board-meetings/decisions.md` **(Layer 2 ONLY — never raw transcripts)**
3. Reset session state — no bleed from previous conversations
4. Present agenda + activated roles → wait for founder confirmation
**Chief of Staff selects relevant roles** based on topic (not all 9 every time):
| Topic | Activate |
|-------|----------|
| Market expansion | CEO, CMO, CFO, CRO, COO |
| Product direction | CEO, CPO, CTO, CMO |
| Hiring/org | CEO, CHRO, CFO, COO |
| Pricing | CMO, CFO, CRO, CPO |
| Technology | CTO, CPO, CFO, CISO |
---
### PHASE 2: Independent Contributions (ISOLATED)
**No cross-pollination. Each agent runs before seeing others' outputs.**
Order: Research (if needed) → CMO → CFO → CEO → CTO → COO → CHRO → CRO → CISO → CPO
**Reasoning techniques:** CEO: Tree of Thought (3 futures) | CFO: Chain of Thought (show the math) | CMO: Recursion of Thought (draft→critique→refine) | CPO: First Principles | CRO: Chain of Thought (pipeline math) | COO: Step by Step (process map) | CTO: ReAct (research→analyze→act) | CISO: Risk-Based (P×I) | CHRO: Empathy + Data
**Contribution format (max 5 key points, self-verified):**
```
## [ROLE] — [DATE]
Key points (max 5):
• [Finding] — [VERIFIED/ASSUMED] — 🟢/🟡/🔴
• [Finding] — [VERIFIED/ASSUMED] — 🟢/🟡/🔴
Recommendation: [clear position]
Confidence: High / Medium / Low
Source: [where the data came from]
What would change my mind: [specific condition]
```
Each agent self-verifies before contributing: source attribution, assumption audit, confidence scoring. No untagged claims.
---
### PHASE 3: Critic Analysis
Executive Mentor receives ALL Phase 2 outputs simultaneously. Role: adversarial reviewer, not synthesizer.
Checklist:
- Where did agents agree too easily? (suspicious consensus = red flag)
- What assumptions are shared but unvalidated?
- Who is missing from the room? (customer voice? front-line ops?)
- What risk has nobody mentioned?
- Which agent operated outside their domain?
---
### PHASE 4: Synthesis
Chief of Staff delivers using the **Board Meeting Output** format (defined in `agent-protocol/SKILL.md`):
- Decision Required (one sentence)
- Perspectives (one line per contributing role)
- Where They Agree / Where They Disagree
- Critic's View (the uncomfortable truth)
- Recommended Decision + Action Items (owners, deadlines)
- Your Call (options if founder disagrees)
---
### PHASE 5: Human in the Loop ⏸️
**Full stop. Wait for the founder.**
```
⏸️ FOUNDER REVIEW — [Paste synthesis]
Options: ✅ Approve | ✏️ Modify | ❌ Reject | ❓ Ask follow-up
```
**Rules:**
- User corrections OVERRIDE agent proposals. No pushback. No "but the CFO said..."
- 30-min inactivity → auto-close as "pending review"
- Reopen any time with `/cs:board resume`
---
### PHASE 6: Decision Extraction
After founder approval:
- **Layer 1:** Write full transcript → `memory/board-meetings/YYYY-MM-DD-raw.md`
- **Layer 2:** Append approved decisions → `memory/board-meetings/decisions.md`
- Mark rejected proposals `[DO_NOT_RESURFACE]`
- Confirm to founder with count of decisions logged, actions tracked, flags added
---
## Memory Structure
```
memory/board-meetings/
├── decisions.md # Layer 2 — founder-approved only (Phase 1 loads this)
├── YYYY-MM-DD-raw.md # Layer 1 — full transcripts (never auto-loaded)
└── archive/YYYY/ # Raw transcripts after 90 days
```
**Future meetings load Layer 2 only.** Never Layer 1. This prevents hallucinated consensus.
---
## Failure Mode Quick Reference
| Failure | Fix |
|---------|-----|
| Groupthink (all agree) | Re-run Phase 2 isolated; force "strongest argument against" |
| Analysis paralysis | Cap at 5 points; force recommendation even with Low confidence |
| Bikeshedding | Log as async action item; return to main agenda |
| Role bleed (CFO making product calls) | Critic flags; exclude from synthesis |
| Layer contamination | Phase 1 loads decisions.md only — hard rule |
---
## References
- `templates/meeting-agenda.md` — agenda format
- `templates/meeting-minutes.md` — final output format
- `references/meeting-facilitation.md` — conflict handling, timing, failure modes
FILE:board-meeting/references/meeting-facilitation.md
# Meeting Facilitation Guide
Operational playbook for running board meetings using the 6-phase protocol.
Reference this when things go sideways — and they will.
---
## Keeping Phase 2 Contributions Focused
**The problem:** Agents with deep domain knowledge tend to over-contribute. An unconstrained CFO can produce 1,500 words on a single agenda item. This kills the meeting.
**The rules:**
- **Hard cap: 5 key points per role.** If a role produces more than 5, Chief of Staff trims to the 5 most material.
- **Every point must include a recommendation or stance.** Observations without positions are filler.
- **No hedging language.** "It depends" is not a key point. "We should do X if Y, Z if not Y" is.
- **Confidence rating required.** Forces the agent to be honest about what they actually know.
- **"What would change my mind"** — this is the most important line in the contribution. It forces falsifiability.
**How to enforce:**
```
Chief of Staff instruction to each role:
"You have 5 key points maximum. Each must include a clear stance.
End with your recommendation and what would change your mind.
Do not read other agents' contributions before writing yours."
```
**If a contribution runs long:**
- Trim to the 5 highest-signal points
- Preserve the recommendation and confidence rating
- Flag in the raw transcript: "[Trimmed for meeting — full version in raw log]"
---
## Handling Role Conflicts in Phase 3
**What the Executive Mentor is for:** Not harmony. Not consensus. Productive friction.
**Common conflict types:**
### 1. Data conflict (two agents cite contradictory numbers)
- Flag both numbers explicitly
- Do NOT pick a winner — that's the founder's job
- Ask: "CFO says CAC is $2,400. CRO says $1,800. These can't both be right. Which dataset are you using?"
- Action item: Assign data reconciliation to one owner before next meeting
### 2. Priority conflict (two agents want different things first)
- Surface the underlying assumption difference
- Example: "CMO wants to invest in brand. CFO wants to cut burn. The real question is: do we believe revenue will grow 40% next quarter?"
- Frame as a bet, not a fight
### 3. Role conflict (agent operating outside their lane)
- CFO making product calls → flag and exclude from synthesis
- CMO commenting on architecture → flag and exclude
- The Executive Mentor notes: "[ROLE] contribution on [topic] is outside domain. Excluded from synthesis. Refer to [correct role]."
- This is not an error. It's expected. Executives have opinions on everything. Only domain-relevant contributions count.
### 4. False consensus (everyone agrees but nobody has evidence)
- This is the most dangerous failure mode
- Symptom: All Phase 2 contributions say "yes" with high confidence
- Executive Mentor response: "Unanimous agreement on a hard question is a red flag. What evidence does each of you have? Or are you reasoning from the same assumption?"
- Force each agreeing agent to state their independent evidence
---
## When to Extend vs Cut Short a Meeting
**Extend when:**
- A genuine new risk surfaces in Phase 3 that wasn't in the agenda
- The founder asks a question that requires re-running Phase 2 for a new angle
- A data conflict is discovered that changes the decision space entirely
- The action items from synthesis are unclear or unowned
**How to extend:** Add a new mini-Phase 2 with only the relevant roles for the new question. Don't restart the full meeting.
**Cut short when:**
- The founder has already reached a decision before Phase 4 — capture it, log it, move on
- The agenda item is resolved in Phase 2 without genuine conflict — skip Phase 3, go straight to synthesis
- It's a pure update meeting with no decisions required — skip Phases 2-4, go straight to action items
**Never cut short:**
- Phase 5 (founder review) — always required, always explicit
- Phase 6 (decision extraction) — always required, even for small decisions
---
## Handling Founder Disagreement with All Agents
This happens. The founder has context agents don't.
**Protocol:**
1. Acknowledge explicitly: "You're overriding the consensus position."
2. Ask: "What do you know that the agents didn't factor in?" (Not to challenge — to capture.)
3. Log the override in Layer 2 with full context:
```
User Override: Founder rejected [consensus position] because [reason].
Decision: [founder's actual decision]
Agent recommendation: [what they said] — DO NOT RESURFACE without new data
```
4. Never push back on a founder override. Document it. Move on.
5. If the same override happens 3+ times, flag a pattern: "You've overridden the CFO on burn rate three meetings in a row. Would you like to update the financial constraints in company-context.md?"
**What NOT to do:**
- Don't say "but the CFO said..."
- Don't re-argue on behalf of any agent
- Don't note it as a "controversial" decision in the minutes — it's just the decision
---
## Common Failure Modes
### Groupthink
**Symptom:** All agents produce similar recommendations with high confidence.
**Cause:** Agents are inadvertently reading each other's outputs (Phase 2 isolation violated), or company-context.md contains implicit bias toward one direction.
**Fix:** Re-run Phase 2 with explicit isolation. Ask: "Give me the strongest argument AGAINST this direction."
### Analysis Paralysis
**Symptom:** Phase 2 produces comprehensive analysis but no clear recommendation from any role.
**Cause:** Agents are hedging. Usually happens on genuinely hard questions.
**Fix:** Force the issue. "I need a recommendation, not an analysis. If you had to bet the company on one direction, what would it be? Confidence can be Low."
### Bikeshedding
**Symptom:** 30+ minutes spent on a detail that doesn't matter to the core decision.
**Cause:** An easy-to-understand sub-problem attracts disproportionate attention.
**Example:** Debating button color on a pricing page instead of the pricing strategy.
**Fix:** Chief of Staff intervenes: "This is a sub-decision. I'm logging it as a separate action item for async resolution. Back to [main agenda item]."
### Scope Creep
**Symptom:** New agenda items keep appearing mid-meeting.
**Cause:** Meeting surfaces real issues that feel urgent.
**Fix:** New items go on a "parking lot" list. Addressed after the current agenda is complete or in the next meeting.
```
🅿️ PARKING LOT
- [Item 1] — added by [role], will address [when]
- [Item 2]
```
### Layer Contamination
**Symptom:** Future meeting references a rejected proposal or a debate that was never approved.
**Cause:** Phase 1 accidentally loaded a raw transcript instead of decisions.md.
**Fix:** Hard rule in Phase 1: load decisions.md (Layer 2) ONLY. Never load raw transcripts. If raw context is needed, founder explicitly requests it.
### Decision Amnesia
**Symptom:** Same question debated again in a later meeting.
**Cause:** Layer 2 decisions.md not consulted in Phase 1, or entry was too vague.
**Fix:** Phase 1 always surfaces relevant past decisions. If a question was already decided, Chief of Staff surfaces it: "We addressed this on [DATE]. Decision was [X]. Do you want to reopen it?"
### Role Fatigue
**Symptom:** Later agents in Phase 2 (CHRO, CRO) produce weaker contributions.
**Cause:** Context window pressure. Agents at the end of a long meeting have less capacity.
**Fix:** For meetings with 7+ roles, split into two batches. First batch: strategic roles (CEO, CFO, CMO). Second batch: operational roles (COO, CHRO, CRO). Run Executive Mentor after all contributions.
---
## Meeting Health Metrics
After each board meeting, score it:
| Metric | Good | Bad |
|--------|------|-----|
| Action items produced | 3–7 | 0 or >10 |
| Decisions with clear owners | 100% | < 80% |
| Unresolved open questions | 1–3 | >5 |
| Founder overrides | 0–2 | >5 (suggests context mismatch) |
| Roles activated | 3–6 | All 9 (too many = noise) |
| Phase 2 conflicts surfaced | At least 1 | 0 (groupthink risk) |
Track these in `memory/board-meetings/meeting-health.md` over time. Pattern: if action items consistently exceed 8, meetings are too infrequent. If conflicts are consistently 0, isolation is broken.
FILE:board-meeting/templates/meeting-agenda.md
# Board Meeting Agenda Template
Use this to structure a board meeting before invoking `/cs:board`.
Paste it into the conversation or save it as `memory/board-meetings/agenda-YYYY-MM-DD.md`.
---
## Board Meeting — [DATE]
**Convened by:** [Founder name]
**Facilitator:** Chief of Staff (Leo)
**Duration:** [estimated, e.g., 45–90 min]
**Status:** Draft / Confirmed
---
## Standing Items (always included)
| Item | Owner | Time |
|------|-------|------|
| Layer 2 decisions review (what changed since last meeting) | Chief of Staff | 5 min |
| Open action items from last meeting | All | 10 min |
| Blockers requiring founder decision | All | 5 min |
---
## Agenda Items
### Item 1: [Title]
**Type:** Decision required / Exploration / Update
**Lead role(s):** [e.g., CEO + CFO]
**Context:** [1-2 sentences on why this is on the agenda now]
**Decision needed:** [What specifically must be decided, or what question must be answered]
**Success criteria:** [How will we know this agenda item is resolved?]
**Relevant past decisions:** [Reference any Layer 2 entries]
**Time box:** [e.g., 20 min]
---
### Item 2: [Title]
**Type:** Decision required / Exploration / Update
**Lead role(s):**
**Context:**
**Decision needed:**
**Success criteria:**
**Relevant past decisions:**
**Time box:**
---
### Item 3: [Title]
**Type:** Decision required / Exploration / Update
**Lead role(s):**
**Context:**
**Decision needed:**
**Success criteria:**
**Relevant past decisions:**
**Time box:**
---
## Out of Scope (explicitly excluded)
List topics that might come up but are NOT on today's agenda:
- [Topic] — defer to [date or next meeting]
- [Topic] — owner to handle async
---
## Pre-Read
Materials all participants should review before the meeting:
- [ ] `memory/board-meetings/decisions.md` (Chief of Staff loads automatically)
- [ ] [Link or filename]
- [ ] [Link or filename]
---
## Notes
[Any special instructions, constraints, or context for this meeting]
FILE:board-meeting/templates/meeting-minutes.md
# Board Meeting Minutes Template
This is the Layer 2 output — the founder-approved record of what was decided.
Written by Chief of Staff after Phase 5 (founder approval).
Appended to `memory/board-meetings/decisions.md`.
Do NOT include raw agent debate here. That lives in `YYYY-MM-DD-raw.md` (Layer 1).
---
## Board Meeting — [DATE]
**Agenda:** [Topic or meeting title]
**Participants (roles activated):** [e.g., CEO, CFO, CMO, COO, Executive Mentor]
**Facilitator:** Chief of Staff
**Status:** ✅ Approved by founder / ⏸️ Pending review
---
## Decisions Made
### Decision 1: [Title]
**Agenda item:** [Item this decision resolves]
**Decision:** [Exactly what was decided — one clear statement]
**Rationale:** [Why this was chosen over alternatives, in 1-3 sentences]
**Owner:** [Who is accountable for execution]
**Deadline:** [Date]
**Review date:** [When to check progress]
**User override:** [If founder overrode agent consensus — what and why. Leave blank if not applicable.]
---
### Decision 2: [Title]
**Agenda item:**
**Decision:**
**Rationale:**
**Owner:**
**Deadline:**
**Review date:**
**User override:**
---
## Action Items
| # | Action | Owner | Deadline | Review Date | Status |
|---|--------|-------|----------|-------------|--------|
| 1 | [action] | [name/role] | [date] | [date] | Open |
| 2 | [action] | [name/role] | [date] | [date] | Open |
| 3 | [action] | [name/role] | [date] | [date] | Open |
---
## Explicitly Rejected Proposals
These were considered and rejected. Do not resurface without new information.
| Proposal | Rejected by | Reason | Flag |
|----------|-------------|--------|------|
| [Proposal text] | Founder | [reason] | [DO_NOT_RESURFACE] |
| [Proposal text] | Consensus | [reason] | [DO_NOT_RESURFACE] |
---
## Open Questions (unresolved, deferred)
These were not resolved in this meeting. They carry forward.
1. [Question] — Owner: [who will research] — Due: [date]
2. [Question] — Owner: — Due:
---
## Risk Register Updates
| Risk | Probability | Impact | Owner | Mitigation | Status |
|------|-------------|--------|-------|-----------|--------|
| [risk] | H/M/L | H/M/L | [name] | [action] | Open |
---
## Next Meeting
**Suggested date:** [DATE]
**Trigger items:** [Action items with review dates that will need board discussion]
**Pre-read:** [What to prepare]
---
*Minutes approved by: [Founder name] on [DATE]*
*Raw transcript: `memory/board-meetings/[DATE]-raw.md`*
FILE:c_level_leadership_skills_overview.md
# C-Level Leadership Skills Suite
## Executive Summary
Two comprehensive leadership skills have been created for your executive team:
### 1. CTO Advisor ✅
Strategic technology leadership skill providing frameworks for architecture decisions, team scaling, technical debt management, and engineering excellence.
### 2. CEO Advisor ✅
Comprehensive executive leadership skill providing strategic planning, financial modeling, board governance, investor relations, and organizational transformation tools.
## Skill Components Overview
### CTO Advisor Skill
**Scripts Included**:
- `tech_debt_analyzer.py` - Analyzes technical debt and prioritizes reduction strategies
- `team_scaling_calculator.py` - Optimizes engineering team growth and structure
**Reference Frameworks**:
- Architecture Decision Records (ADR) framework
- Technology evaluation and vendor selection
- Engineering metrics and KPIs (DORA metrics)
**Key Capabilities**:
- Technical debt assessment and prioritization
- Engineering team scaling optimization
- Architecture decision documentation
- Technology vendor evaluation
- Engineering performance measurement
### CEO Advisor Skill
**Scripts Included**:
- `strategy_analyzer.py` - Comprehensive strategic position analysis
- `financial_scenario_analyzer.py` - Multi-scenario financial modeling
**Reference Frameworks**:
- Executive decision-making frameworks
- Board governance and investor relations
- Leadership and organizational culture
**Key Capabilities**:
- Strategic planning and analysis
- Financial scenario modeling
- Board meeting management
- Investor communication
- Organizational transformation
## Implementation Guide
### Phase 1: Deployment (Week 1)
#### For CTO
1. Deploy `cto-advisor.zip`
2. Run technical debt assessment
3. Evaluate current team structure
4. Review architecture decisions
5. Implement DORA metrics
#### For CEO
1. Deploy `ceo-advisor.zip`
2. Run strategic analysis
3. Model financial scenarios
4. Review board processes
5. Assess organizational culture
### Phase 2: Integration (Weeks 2-4)
#### Cross-Functional Alignment
- Align technology strategy with business strategy
- Coordinate resource allocation
- Synchronize roadmaps
- Establish joint KPIs
#### Process Implementation
- Weekly leadership sync
- Monthly board reporting
- Quarterly strategic reviews
- Annual planning cycles
### Phase 3: Optimization (Month 2+)
#### Continuous Improvement
- Refine frameworks based on usage
- Customize scripts for specific needs
- Develop company-specific templates
- Build institutional knowledge
## Use Case Scenarios
### CTO Scenarios
#### Scenario 1: Technical Debt Crisis
```bash
# Assess current technical debt
python scripts/tech_debt_analyzer.py
# Output: Prioritized action plan
# Timeline: 3-18 months
# Investment: $X based on debt level
```
#### Scenario 2: Rapid Team Scaling
```bash
# Plan optimal team growth
python scripts/team_scaling_calculator.py
# Input: Current 25 → Target 75 engineers
# Output: Quarterly hiring plan, structure, budget
```
#### Scenario 3: Architecture Decision
- Use ADR template from references
- Document context, options, decision
- Track consequences and learnings
### CEO Scenarios
#### Scenario 1: Strategic Planning
```bash
# Analyze strategic position
python scripts/strategy_analyzer.py
# Output: Health score, options, roadmap
# Focus areas identified
# 18-month implementation plan
```
#### Scenario 2: Fundraising Preparation
```bash
# Model growth scenarios
python scripts/financial_scenario_analyzer.py
# Three scenarios: Conservative, Base, Aggressive
# NPV, IRR, break-even analysis
# Risk-adjusted recommendations
```
#### Scenario 3: Board Meeting Prep
- Use board package template
- Prepare governance materials
- Structure executive session topics
## Key Metrics & KPIs
### Technology Metrics (CTO)
**Engineering Performance**:
- Deployment frequency: >1/day
- Lead time: <1 day
- MTTR: <1 hour
- Change failure rate: <15%
**Team Health**:
- Velocity stability: ±10%
- Code coverage: >80%
- Technical debt: <10%
- Attrition: <10%
**System Performance**:
- Uptime: >99.9%
- Response time: <200ms
- Error rate: <0.1%
- Scalability: Linear
### Business Metrics (CEO)
**Financial Performance**:
- Revenue growth: >30% YoY
- Gross margin: >70%
- EBITDA positive by Y2
- Cash runway: >18 months
**Organizational Health**:
- Employee NPS: >50
- Customer NPS: >70
- Board confidence: High
- Culture score: >8/10
**Strategic Progress**:
- OKR achievement: >70%
- Market share growth
- Innovation pipeline
- Strategic initiatives on track
## Synergy Opportunities
### CTO-CEO Collaboration Points
#### 1. Strategic Alignment
- Technology enables business strategy
- Business priorities drive tech investments
- Joint roadmap development
- Shared success metrics
#### 2. Resource Optimization
- Coordinated budget planning
- Talent strategy alignment
- Vendor consolidation
- Investment prioritization
#### 3. Risk Management
- Technical risk assessment
- Business continuity planning
- Security and compliance
- Crisis response protocols
#### 4. Innovation Drive
- R&D investment strategy
- Digital transformation
- Competitive differentiation
- Future-proofing
## Best Practices
### For Effective Usage
#### Daily Habits
- Review key metrics dashboard
- Check strategic alignment
- Address critical decisions
- Communicate priorities
#### Weekly Rituals
- Leadership team sync
- Progress reviews
- Stakeholder updates
- Course corrections
#### Monthly Processes
- Deep strategic reviews
- Financial analysis
- Organizational health check
- Board preparation
#### Quarterly Milestones
- Strategy adjustment
- Performance evaluation
- Planning cycles
- Stakeholder engagement
### Common Pitfalls to Avoid
#### CTO Pitfalls
- Over-engineering solutions
- Ignoring technical debt
- Scaling too fast/slow
- Misaligned architecture
#### CEO Pitfalls
- Analysis paralysis
- Poor stakeholder management
- Culture neglect
- Reactive leadership
## ROI Analysis
### CTO Advisor ROI
**Time Savings**:
- Decision making: 50% faster
- Team planning: 60% more accurate
- Architecture reviews: 40% more efficient
**Cost Benefits**:
- Technical debt reduction: $2M+ saved
- Optimal scaling: 30% lower hiring costs
- Better decisions: Avoid $5M+ mistakes
**Quality Improvements**:
- System reliability: 99.9%+
- Team productivity: +25%
- Architecture quality: Significantly improved
### CEO Advisor ROI
**Strategic Value**:
- Better strategic decisions
- Faster execution
- Improved stakeholder confidence
**Financial Impact**:
- Optimized capital allocation
- Better fundraising outcomes
- Increased valuation
**Organizational Benefits**:
- Stronger culture
- Better talent retention
- Higher performance
## Success Stories (Projected)
### After 3 Months
- Technical debt reduced by 30%
- Engineering team optimally structured
- Strategic clarity achieved
- Board relationships strengthened
### After 6 Months
- DORA metrics at "Elite" level
- Team scaling on track
- Strategic initiatives delivering
- Culture transformation visible
### After 12 Months
- Engineering excellence achieved
- Organizational capabilities transformed
- Market position strengthened
- Sustainable growth established
## Tool Integration
### Recommended Stack
#### For CTO
- **Metrics**: DataDog, New Relic
- **Planning**: Jira, Linear
- **Architecture**: Draw.io, Confluence
- **Code**: GitHub, GitLab
#### For CEO
- **Strategy**: Cascade, Perdoo
- **Financial**: Excel, Causal
- **Board**: Diligent, BoardEffect
- **Communication**: Slack, Notion
## Support & Evolution
### Skill Maintenance
- Regular framework updates
- Script enhancements
- Template refinements
- Best practice sharing
### Community Building
- Leadership forums
- Peer learning
- Case study sharing
- Continuous improvement
### Future Enhancements
#### Potential Additional Skills
- **CFO Advisor**: Financial management, fundraising, investor relations
- **CPO Advisor**: Product strategy, roadmapping, customer insights
- **CMO Advisor**: Marketing strategy, brand, demand generation
- **CHRO Advisor**: Talent strategy, culture, organizational development
## Quick Reference
### CTO Quick Commands
```bash
# Analyze technical debt
python scripts/tech_debt_analyzer.py
# Plan team scaling
python scripts/team_scaling_calculator.py
# Review ADR framework
cat references/architecture_decision_records.md
```
### CEO Quick Commands
```bash
# Analyze strategy
python scripts/strategy_analyzer.py
# Model scenarios
python scripts/financial_scenario_analyzer.py
# Review decision framework
cat references/executive_decision_framework.md
```
## Conclusion
These C-Level leadership skills provide comprehensive frameworks, tools, and guidance for effective executive leadership. By combining strategic thinking with practical tools, they enable faster, better decisions while building stronger organizations.
The synergy between CTO and CEO skills creates a powerful leadership toolkit that addresses both technical and business challenges, ensuring aligned, effective leadership across the organization.
**Files Available**:
- [CTO Advisor Skill](computer:///mnt/user-data/outputs/cto-advisor.zip)
- [CEO Advisor Skill](computer:///mnt/user-data/outputs/ceo-advisor.zip)
Deploy these skills to transform your leadership effectiveness and drive organizational excellence.
FILE:ceo-advisor/SKILL.md
---
name: "ceo-advisor"
description: "Executive leadership guidance for strategic decision-making, organizational development, and stakeholder management. Use when planning strategy, preparing board presentations, managing investors, developing organizational culture, making executive decisions, fundraising, or when user mentions CEO, strategic planning, board meetings, investor updates, organizational leadership, or executive strategy."
license: MIT
metadata:
version: 2.0.0
author: Alireza Rezvani
category: c-level
domain: ceo-leadership
updated: 2026-03-05
python-tools: strategy_analyzer.py, financial_scenario_analyzer.py
frameworks: executive-decisions, board-governance, leadership-culture
---
# CEO Advisor
Strategic leadership frameworks for vision, fundraising, board management, culture, and stakeholder alignment.
## Keywords
CEO, chief executive officer, strategy, strategic planning, fundraising, board management, investor relations, culture, organizational leadership, vision, mission, stakeholder management, capital allocation, crisis management, succession planning
## Quick Start
```bash
python scripts/strategy_analyzer.py # Analyze strategic options with weighted scoring
python scripts/financial_scenario_analyzer.py # Model financial scenarios (base/bull/bear)
```
## Core Responsibilities
### 1. Vision & Strategy
Set the direction. Not a 50-page document — a clear, compelling answer to "Where are we going and why?"
**Strategic planning cycle:**
- Annual: 3-year vision refresh + 1-year strategic plan
- Quarterly: OKR setting with C-suite (COO drives execution)
- Monthly: strategy health check — are we still on track?
**Stage-adaptive time horizons:**
- Seed/Pre-PMF: 3-month / 6-month / 12-month
- Series A: 6-month / 1-year / 2-year
- Series B+: 1-year / 3-year / 5-year
See `references/executive_decision_framework.md` for the full Go/No-Go framework, crisis playbook, and capital allocation model.
### 2. Capital & Resource Management
You're the chief allocator. Every dollar, every person, every hour of engineering time is a bet.
**Capital allocation priorities:**
1. Keep the lights on (operations, must-haves)
2. Protect the core (retention, quality, security)
3. Grow the core (expansion of what works)
4. Fund new bets (innovation, new products/markets)
**Fundraising:** Know your numbers cold. Timing matters more than valuation. See `references/board_governance_investor_relations.md`.
### 3. Stakeholder Leadership
You serve multiple masters. Priority order:
1. Customers (they pay the bills)
2. Team (they build the product)
3. Board/Investors (they fund the mission)
4. Partners (they extend your reach)
### 4. Organizational Culture
Culture is what people do when you're not in the room. It's your job to define it, model it, and enforce it.
See `references/leadership_organizational_culture.md` for culture development frameworks and the CEO learning agenda. Also see `culture-architect/` for the operational culture toolkit.
### 5. Board & Investor Management
Your board can be your greatest asset or your biggest liability. The difference is how you manage them.
See `references/board_governance_investor_relations.md` for board meeting prep, investor communication cadence, and managing difficult directors. Also see `board-deck-builder/` for assembling the actual board deck.
## Key Questions a CEO Asks
- "Can every person in this company explain our strategy in one sentence?"
- "What's the one thing that, if it goes wrong, kills us?"
- "Am I spending my time on the highest-leverage activity right now?"
- "What decision am I avoiding? Why?"
- "If we could only do one thing this quarter, what would it be?"
- "Do our investors and our team hear the same story from me?"
- "Who would replace me if I got hit by a bus tomorrow?"
## CEO Metrics Dashboard
| Category | Metric | Target | Frequency |
|----------|--------|--------|-----------|
| **Strategy** | Annual goals hit rate | > 70% | Quarterly |
| **Revenue** | ARR growth rate | Stage-dependent | Monthly |
| **Capital** | Months of runway | > 12 months | Monthly |
| **Capital** | Burn multiple | < 2x | Monthly |
| **Product** | NPS / PMF score | > 40 NPS | Quarterly |
| **People** | Regrettable attrition | < 10% | Monthly |
| **People** | Employee engagement | > 7/10 | Quarterly |
| **Board** | Board NPS (your relationship) | Positive trend | Quarterly |
| **Personal** | % time on strategic work | > 40% | Weekly |
## Red Flags
- You're the bottleneck for more than 3 decisions per week
- The board surprises you with questions you can't answer
- Your calendar is 80%+ meetings with no strategic blocks
- Key people are leaving and you didn't see it coming
- You're fundraising reactively (runway < 6 months, no plan)
- Your team can't articulate the strategy without you in the room
- You're avoiding a hard conversation (co-founder, investor, underperformer)
## Integration with C-Suite Roles
| When... | CEO works with... | To... |
|---------|-------------------|-------|
| Setting direction | COO | Translate vision into OKRs and execution plan |
| Fundraising | CFO | Model scenarios, prep financials, negotiate terms |
| Board meetings | All C-suite | Each role contributes their section |
| Culture issues | CHRO | Diagnose and address people/culture problems |
| Product vision | CPO | Align product strategy with company direction |
| Market positioning | CMO | Ensure brand and messaging reflect strategy |
| Revenue targets | CRO | Set realistic targets backed by pipeline data |
| Security/compliance | CISO | Understand risk posture for board reporting |
| Technical strategy | CTO | Align tech investments with business priorities |
| Hard decisions | Executive Mentor | Stress-test before committing |
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- Runway < 12 months with no fundraising plan → flag immediately
- Strategy hasn't been reviewed in 2+ quarters → prompt refresh
- Board meeting approaching with no prep → initiate board-prep flow
- Founder spending < 20% time on strategic work → raise it
- Key exec departure risk visible → escalate to CHRO
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Help me think about strategy" | Strategic options matrix with risk-adjusted scoring |
| "Prep me for the board" | Board narrative + anticipated questions + data gaps |
| "Should we raise?" | Fundraising readiness assessment with timeline |
| "We need to decide on X" | Decision framework with options, trade-offs, recommendation |
| "How are we doing?" | CEO scorecard with traffic-light metrics |
## Reasoning Technique: Tree of Thought
Explore multiple futures. For every strategic decision, generate at least 3 paths. Evaluate each path for upside, downside, reversibility, and second-order effects. Pick the path with the best risk-adjusted outcome.
**Stage-adaptive horizons:**
- Seed: project 3m/6m/12m
- Series A: project 6m/1y/2y
- Series B+: project 1y/3y/5y
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
## Resources
- `references/executive_decision_framework.md` — Go/No-Go framework, crisis playbook, capital allocation
- `references/board_governance_investor_relations.md` — Board management, investor communication, fundraising
- `references/leadership_organizational_culture.md` — Culture development, CEO routines, succession planning
FILE:ceo-advisor/references/board_governance_investor_relations.md
# Board Governance & Investor Relations Guide
## Board of Directors Management
### Board Composition
#### Ideal Board Structure
- **Size**: 7-9 members (odd number for voting)
- **Independence**: Majority independent directors
- **Diversity**: Gender, ethnicity, expertise, experience
- **Term**: 3-year terms, staggered renewal
#### Board Roles
| Role | Responsibilities | Typical Background |
|------|-----------------|-------------------|
| Chairman | Board leadership, CEO liaison | Former CEO, Industry veteran |
| Lead Independent Director | Independent voice, executive sessions | Senior executive experience |
| Audit Committee Chair | Financial oversight, auditor relationship | CFO/CPA background |
| Compensation Committee Chair | Executive compensation, succession | HR/Executive experience |
| Nominating Committee Chair | Board composition, governance | Governance expertise |
### Board Meeting Management
#### Annual Board Calendar
**Q1 Meeting**
- Annual strategy review
- Previous year performance
- Current year priorities
- Risk assessment update
**Q2 Meeting**
- Q1 results review
- Strategic initiative progress
- Competitive landscape
- Talent review
**Q3 Meeting**
- Mid-year performance
- Budget preview
- Strategic planning session
- Succession planning
**Q4 Meeting**
- Annual budget approval
- Executive compensation
- Board evaluation
- Upcoming year calendar
#### Meeting Preparation Timeline
**T-4 Weeks**
- Agenda draft to Chairman
- Pre-read preparation begins
- Committee meetings scheduled
**T-2 Weeks**
- Materials to review committee
- Final agenda confirmation
- Logistics coordination
**T-1 Week**
- Board package distribution
- Pre-meeting calls as needed
- Final preparations
**T-0 Meeting Day**
- Executive session (start)
- Board meeting
- Executive session (end)
- Follow-up actions defined
### Board Package Template
#### Standard Package Contents
1. **Cover Memo** (1 page)
- Meeting agenda
- Key decisions required
- Time allocations
2. **CEO Report** (3-5 pages)
- Executive summary
- Performance highlights
- Strategic progress
- Key challenges
- Asks of the board
3. **Financial Report** (5-10 pages)
- Financial statements
- KPI dashboard
- Variance analysis
- Cash position
- Forecast update
4. **Strategic Updates** (10-15 pages)
- Initiative status
- Market analysis
- Competitive intelligence
- Product roadmap
5. **Committee Reports** (2-3 pages each)
- Audit Committee
- Compensation Committee
- Other committees
6. **Appendices**
- Detailed financials
- Supporting analysis
- Previous minutes
### Board Communication Best Practices
#### Between Meetings
**Monthly Update Email**
```
Subject: [Company] CEO Update - [Month Year]
Board Members,
Quick update on [Month] performance:
Headlines:
• [Key achievement]
• [Important metric]
• [Strategic progress]
Challenges:
• [Issue and mitigation]
Looking Ahead:
• [Upcoming milestone]
Detailed dashboard attached.
Best,
[CEO Name]
```
**Flash Reports** (When needed)
- Material events
- Major wins/losses
- Press coverage
- Regulatory matters
#### Managing Difficult Conversations
**Delivering Bad News**
1. Don't delay - inform promptly
2. Lead with facts
3. Own the responsibility
4. Present action plan
5. Set realistic timeline
**Handling Dissent**
1. Listen fully
2. Acknowledge concerns
3. Provide data/rationale
4. Seek common ground
5. Document decisions
## Investor Relations
### Investor Segmentation
#### Institutional Investors
**Types**:
- Mutual funds
- Pension funds
- Hedge funds
- Private equity
- Sovereign wealth funds
**Engagement Strategy**:
- Quarterly earnings calls
- Annual investor day
- Conference participation
- One-on-one meetings
- Site visits
#### Retail Investors
**Channels**:
- Website IR section
- Annual reports
- Proxy statements
- Social media
- Shareholder meetings
### Earnings Communications
#### Earnings Release Template
```
[COMPANY] REPORTS [QUARTER] [YEAR] RESULTS
[City, Date] - [Company] (TICKER) today reported results for [quarter]:
Financial Highlights:
• Revenue: $X (±Y% YoY)
• Net Income: $X (±Y% YoY)
• EPS: $X (±Y% YoY)
• [Other key metric]
CEO Commentary:
"[Quote about performance and outlook]"
CFO Commentary:
"[Quote about financial details]"
Guidance:
[Forward-looking statements]
Conference Call:
Date/Time: [Details]
Webcast: [Link]
About [Company]:
[Boilerplate]
Contact:
[IR contact information]
```
#### Earnings Call Script Structure
**CEO Opening (5 minutes)**
```
Good [morning/afternoon], and welcome to [Company's]
[Quarter] earnings call.
Today I'll cover:
1. Quarter highlights
2. Strategic progress
3. Market dynamics
4. Outlook
[Key points with supporting data]
I'll now turn it over to our CFO...
```
**CFO Section (10 minutes)**
```
Thank you [CEO name].
Financial Performance:
- Revenue details by segment
- Margin analysis
- Cash flow review
- Balance sheet highlights
Guidance:
- Next quarter expectations
- Full year outlook
- Key assumptions
Now back to [CEO] for closing remarks...
```
**Q&A Management**
- Anticipate top 10 questions
- Prepare fact sheets
- Designate responders
- Bridge to key messages
- Time management
### Investor Messaging Framework
#### Value Proposition
**Investment Thesis Elements**:
1. Market opportunity size
2. Competitive advantages
3. Growth strategy
4. Financial model
5. Management team
6. Risk factors
#### Key Messages Architecture
**Primary Messages** (Memorize)
1. [Core value proposition]
2. [Differentiation]
3. [Growth trajectory]
**Supporting Points** (Have ready)
- Market data
- Customer proof points
- Financial metrics
- Strategic initiatives
**Proof Points** (Document)
- Case studies
- Metrics
- Third-party validation
- Awards/recognition
### Investor Day Planning
#### 6-Month Planning Timeline
**T-6 Months**
- Set date and venue
- Define objectives
- Identify speakers
- Begin content development
**T-4 Months**
- Develop presentations
- Coordinate logistics
- Begin rehearsals
- Create save-the-date
**T-2 Months**
- Finalize content
- Complete rehearsals
- Send invitations
- Prepare materials
**T-1 Month**
- Final preparations
- Media training
- Q&A preparation
- Technology testing
**T-0 Event Day**
- Execute program
- Manage Q&A
- Network sessions
- Follow-up plan
#### Agenda Template
```
8:00 AM - Registration & Breakfast
8:30 AM - CEO Welcome & Vision
9:00 AM - Market Opportunity
9:30 AM - Product Strategy & Demo
10:00 AM - Break
10:15 AM - Go-to-Market Strategy
10:45 AM - Financial Overview
11:15 AM - Q&A Panel
12:00 PM - Networking Lunch
1:00 PM - Facility Tour (Optional)
```
### Shareholder Activism Defense
#### Early Warning Signs
- Stake building (13D/13G filings)
- Public criticism
- Media campaigns
- Proxy solicitation
- Shareholder proposals
#### Response Playbook
**1. Preparation Phase**
- Vulnerability assessment
- Response team formation
- Advisor engagement
- Board alignment
**2. Engagement Phase**
- Direct dialogue
- Understanding demands
- Finding common ground
- Negotiation strategy
**3. Defense Phase** (if needed)
- Public response
- Proxy fight preparation
- Shareholder outreach
- Media strategy
**4. Resolution Phase**
- Settlement negotiations
- Implementation planning
- Communication strategy
- Monitoring plan
### Regulatory Compliance
#### Key Filings
| Form | Purpose | Timing |
|------|---------|--------|
| 10-K | Annual report | 60-90 days after FY end |
| 10-Q | Quarterly report | 40-45 days after Q end |
| 8-K | Material events | 4 business days |
| DEF 14A | Proxy statement | Before annual meeting |
| S-1/S-3 | Securities registration | As needed |
#### Disclosure Requirements
**Material Information**:
- Financial results
- Major transactions
- Leadership changes
- Strategic shifts
- Legal proceedings
- Risk changes
**Regulation FD Compliance**:
- No selective disclosure
- Simultaneous public release
- Documented procedures
- Training program
### Crisis Communication
#### IR Crisis Response
**Hour 1: Assessment**
- Gather facts
- Assess materiality
- Consult legal
- Prepare holding statement
**Hours 2-4: Response**
- Draft 8-K if required
- Prepare FAQ
- Update website
- Notify exchanges
**Hours 4-8: Communication**
- Issue press release
- Update analysts
- Employee communication
- Monitor reactions
**Day 2+: Follow-up**
- Investor calls
- Media interviews
- Ongoing updates
- Impact assessment
### Performance Metrics
#### IR Effectiveness KPIs
**Quantitative Metrics**:
- Share price performance vs peers
- Trading volume/liquidity
- Analyst coverage
- Institutional ownership %
- Valuation multiples vs peers
**Qualitative Metrics**:
- Analyst sentiment
- Media coverage tone
- Investor feedback
- Award recognition
- Perception studies
#### Shareholder Analysis
**Ownership Tracking**:
- Top 20 shareholders
- Ownership changes
- Peer ownership overlap
- Geographic distribution
- Investment style mix
**Engagement Metrics**:
- Meeting count
- Conference participation
- Earnings call attendance
- Website analytics
- Email engagement
## Governance Best Practices
### Board Effectiveness
#### Annual Board Evaluation
**Process**:
1. Anonymous surveys
2. Individual interviews
3. Peer feedback
4. Results compilation
5. Action planning
6. Progress monitoring
**Evaluation Areas**:
- Board composition
- Meeting effectiveness
- Information quality
- Strategic oversight
- Risk management
- CEO relationship
- Committee performance
### Executive Session Management
**Frequency**: Every board meeting
**Duration**: 30-60 minutes
**Participants**: Independent directors only
**Typical Topics**:
- CEO performance
- Succession planning
- Board dynamics
- Sensitive matters
- Executive compensation
### D&O Insurance & Indemnification
**Coverage Levels**:
- Primary: $10-25M
- Excess: $25-100M+
- Side A: Individual protection
- Side B: Company reimbursement
- Side C: Securities claims
**Best Practices**:
- Annual review
- Competitive benchmarking
- Claims history analysis
- Policy optimization
- Personal coverage consideration
### ESG Governance
#### ESG Integration
**Board Oversight**:
- ESG committee or full board
- Regular ESG updates
- Metrics in dashboard
- Risk assessment
- Stakeholder feedback
**Reporting Framework**:
- SASB standards
- TCFD recommendations
- GRI guidelines
- UN SDGs alignment
- Integrated reporting
**Investor Communication**:
- ESG highlights in earnings
- Dedicated ESG report
- Website ESG section
- ESG investor days
- Rating agency engagement
## Templates & Tools
### Board Resolution Template
```
BOARD RESOLUTION
WHEREAS, [background/context];
WHEREAS, [additional context];
NOW, THEREFORE, BE IT RESOLVED, that [specific action];
FURTHER RESOLVED, that [additional actions];
FURTHER RESOLVED, that [authorization].
Approved this [date].
_____________________
[Secretary Name]
Corporate Secretary
```
### Insider Trading Policy Outline
1. **Scope**: All directors, officers, employees
2. **Prohibited Activities**: Trading on MNPI
3. **Trading Windows**: Quarterly schedule
4. **Pre-clearance**: Required for all trades
5. **Blackout Periods**: Defined schedule
6. **10b5-1 Plans**: Permitted with approval
7. **Violations**: Disciplinary action
8. **Training**: Annual requirement
### Proxy Statement Checklist
- [ ] Executive compensation (CD&A)
- [ ] Director nominees
- [ ] Governance structure
- [ ] Shareholder proposals
- [ ] Audit matters
- [ ] Related party transactions
- [ ] Risk oversight
- [ ] Succession planning
- [ ] ESG disclosure
- [ ] Virtual meeting details
FILE:ceo-advisor/references/executive_decision_framework.md
# Executive Decision Framework
## Decision-Making Process
### The DECIDE Framework
**D** - Define the problem clearly
**E** - Establish criteria for solutions
**C** - Consider alternatives
**I** - Identify best alternatives
**D** - Develop and implement action plan
**E** - Evaluate and monitor solution
## Strategic Decision Categories
### 1. Growth Decisions
#### Market Expansion
**Evaluation Criteria**:
- Market size and growth rate
- Competitive landscape
- Regulatory environment
- Cultural fit
- Required investment
- Expected ROI
**Decision Matrix**:
| Factor | Weight | Score (1-10) | Weighted Score |
|--------|--------|--------------|----------------|
| Market Size | 25% | | |
| Competition | 20% | | |
| Fit with Core | 20% | | |
| Investment Required | 15% | | |
| Risk Level | 10% | | |
| Timeline to Profit | 10% | | |
#### Product Development
**Go/No-Go Criteria**:
- Customer demand validation (>70% interest)
- Technical feasibility confirmed
- Positive unit economics
- Strategic alignment
- Available resources
#### Mergers & Acquisitions
**Due Diligence Framework**:
1. **Strategic Fit**
- Synergies identification
- Cultural alignment
- Market position enhancement
2. **Financial Analysis**
- Valuation models (DCF, Multiples, Precedent)
- ROI projections
- Integration costs
3. **Risk Assessment**
- Legal/regulatory issues
- Technology compatibility
- Talent retention
4. **Integration Planning**
- 100-day plan
- Communication strategy
- Success metrics
### 2. Resource Allocation
#### Capital Allocation Framework
**Priority Levels**:
1. **Essential** - Core operations, compliance, security
2. **Strategic** - Growth initiatives, competitive advantage
3. **Efficiency** - Cost reduction, productivity
4. **Experimental** - Innovation, R&D
**Allocation Guidelines**:
- Essential: 40-50%
- Strategic: 30-40%
- Efficiency: 10-15%
- Experimental: 5-10%
#### Budget Decision Tree
```
Is it required for operations?
├─ Yes → Essential (Auto-approve if <$X)
└─ No → Does it drive growth?
├─ Yes → What's the ROI?
│ ├─ >30% → Strategic (Approve)
│ └─ <30% → Defer/Reject
└─ No → Does it reduce costs?
├─ Yes → Payback period?
│ ├─ <12 months → Efficiency (Approve)
│ └─ >12 months → Defer
└─ No → Experimental (Limited budget)
```
### 3. Organizational Decisions
#### Restructuring Framework
**Triggers for Restructuring**:
- Performance below targets for 2+ quarters
- Major strategic shift
- M&A integration
- Market disruption
- Efficiency opportunity >20%
**Evaluation Process**:
1. Current state assessment
2. Future state design
3. Gap analysis
4. Impact assessment
5. Implementation planning
6. Communication strategy
#### Leadership Changes
**Performance Evaluation Matrix**:
| Dimension | Weight | Indicators |
|-----------|--------|------------|
| Results Delivery | 40% | KPIs, OKRs achievement |
| Team Leadership | 25% | Engagement, retention, development |
| Strategic Thinking | 20% | Innovation, vision, planning |
| Culture Fit | 15% | Values alignment, collaboration |
**Succession Planning**:
- Identify 2-3 potential successors for each key role
- Development plans for high-potentials
- Emergency succession protocols
- Knowledge transfer processes
### 4. Crisis Management
#### Crisis Response Protocol
**Immediate (0-2 hours)**:
1. Activate crisis team
2. Assess severity and impact
3. Implement containment measures
4. Initial stakeholder notification
**Short-term (2-24 hours)**:
1. Develop response strategy
2. Prepare public statements
3. Engage legal/regulatory as needed
4. Employee communication
**Recovery (24+ hours)**:
1. Implement solution
2. Monitor progress
3. Stakeholder updates
4. Post-crisis review
#### Crisis Decision Authority
| Crisis Level | Decision Authority | Response Team |
|--------------|-------------------|---------------|
| Level 1 (Minor) | Department Head | Local team |
| Level 2 (Moderate) | C-Suite Member | Cross-functional |
| Level 3 (Major) | CEO | Executive team |
| Level 4 (Critical) | CEO + Board | All hands |
## Decision Support Tools
### 1. SWOT-TOWS Matrix
```
Internal →
↓ Strengths (S) Weaknesses (W)
External
O SO Strategies WO Strategies
p (Leverage) (Improve)
p
o
r
t
T ST Strategies WT Strategies
h (Protect) (Survive)
r
e
a
t
s
```
### 2. BCG Growth-Share Matrix
```
Market Growth Rate
↑
High │ Stars │ Question │
│ │ Marks │
├─────────┼──────────┤
Low │ Cash │ Dogs │
│ Cows │ │
└─────────┴──────────┘
High Low →
Market Share
```
### 3. Risk-Impact Matrix
```
Impact
↑
High │ Mitigate │ Critical │
│ │ Focus │
├──────────┼──────────┤
Low │ Accept │ Monitor │
│ │ │
└──────────┴──────────┘
Low High →
Probability
```
### 4. Eisenhower Matrix
```
Urgency
↑
High │ Do │ Schedule │
│ First │ │
├─────────┼──────────┤
Low │ Delegate│ Eliminate│
│ │ │
└─────────┴──────────┘
High Low →
Importance
```
## Strategic Options Framework
### Porter's Generic Strategies
1. **Cost Leadership**
- Operational excellence
- Economy of scale
- Process optimization
- Supply chain efficiency
2. **Differentiation**
- Unique value proposition
- Premium positioning
- Innovation focus
- Brand strength
3. **Focus**
- Niche markets
- Specialized offerings
- Deep expertise
- Customer intimacy
### Blue Ocean Strategy
**Four Actions Framework**:
- **Eliminate**: Which factors can be eliminated?
- **Reduce**: Which factors should be reduced below industry standard?
- **Raise**: Which factors should be raised above industry standard?
- **Create**: Which factors should be created that the industry has never offered?
## Stakeholder Management
### Stakeholder Mapping
```
Influence/Power
↑
High │ Manage │ Key │
│ Closely │ Players │
├──────────┼──────────┤
Low │ Monitor │ Keep │
│ │ Informed │
└──────────┴──────────┘
Low High →
Interest
```
### Communication Strategy
| Stakeholder | Frequency | Format | Key Messages |
|------------|-----------|--------|--------------|
| Board | Monthly | Report + Meeting | Strategy, Risk, Performance |
| Investors | Quarterly | Earnings Call | Financial, Growth, Outlook |
| Employees | Weekly | All-hands | Vision, Updates, Recognition |
| Customers | Continuous | Multi-channel | Value, Innovation, Support |
| Media | As needed | Press Release | Milestones, Position, Vision |
## Performance Metrics
### Balanced Scorecard
#### Financial Perspective
- Revenue growth rate
- EBITDA margin
- ROE/ROA
- Cash conversion cycle
- Market capitalization
#### Customer Perspective
- Customer satisfaction (NPS)
- Market share
- Customer retention rate
- Customer acquisition cost
- Customer lifetime value
#### Internal Process
- Operational efficiency
- Time to market
- Quality metrics
- Innovation rate
- Process cycle time
#### Learning & Growth
- Employee engagement
- Talent retention
- Training hours per employee
- Leadership pipeline
- Innovation index
## Decision Biases to Avoid
### Cognitive Biases
1. **Confirmation Bias**
- Mitigation: Seek contrarian views
- Tool: Devil's advocate process
2. **Anchoring Bias**
- Mitigation: Multiple estimates
- Tool: Range forecasting
3. **Sunk Cost Fallacy**
- Mitigation: Zero-based thinking
- Tool: Regular portfolio review
4. **Overconfidence Bias**
- Mitigation: Outside view
- Tool: Reference class forecasting
5. **Availability Heuristic**
- Mitigation: Data-driven decisions
- Tool: Systematic analysis
### Decision Hygiene Checklist
- [ ] Problem clearly defined
- [ ] All stakeholders identified
- [ ] Data/evidence gathered
- [ ] Multiple options generated
- [ ] Biases checked
- [ ] Risks assessed
- [ ] Implementation plan created
- [ ] Success metrics defined
- [ ] Review process established
## Executive Communication
### Board Presentation Template
1. **Executive Summary** (1 slide)
- Key achievements
- Critical issues
- Decisions needed
2. **Performance Review** (3-4 slides)
- Financial results
- Operational metrics
- Strategic progress
3. **Market & Competition** (2 slides)
- Market dynamics
- Competitive position
4. **Strategic Initiatives** (3-4 slides)
- Current initiatives
- Results to date
- Next steps
5. **Risk & Mitigation** (2 slides)
- Risk register
- Mitigation actions
6. **Ask of the Board** (1 slide)
- Decisions required
- Support needed
### Investor Relations Framework
**Earnings Call Structure**:
1. Opening remarks (CEO) - 5 min
2. Financial review (CFO) - 10 min
3. Strategic update (CEO) - 10 min
4. Q&A - 30 min
**Key Messages**:
- Performance vs guidance
- Market position
- Growth strategy
- Capital allocation
- Outlook
## Strategic Planning Cycle
### Annual Planning Process
**Q3 - Strategic Review**
- Environmental scan
- Competitive analysis
- Capability assessment
- Strategy refinement
**Q4 - Planning**
- Goal setting
- Budget allocation
- Resource planning
- OKR development
**Q1 - Launch**
- Communication cascade
- Initiative kickoff
- Quick wins
- Baseline metrics
**Q2 - Review**
- Progress assessment
- Course correction
- Mid-year planning
- Performance review
## Exit Strategy Planning
### Exit Options Evaluation
1. **IPO**
- Pros: Maximum valuation, maintain control
- Cons: Regulatory burden, public scrutiny
- Timeline: 12-24 months
2. **Strategic Acquisition**
- Pros: Synergies, quick process
- Cons: Loss of independence, integration risk
- Timeline: 6-12 months
3. **Private Equity**
- Pros: Growth capital, expertise
- Cons: Pressure for returns, loss of control
- Timeline: 3-6 months
4. **Management Buyout**
- Pros: Continuity, culture preservation
- Cons: Limited price, financing challenge
- Timeline: 6-9 months
### Value Creation Levers
1. **Revenue Growth**
- Organic expansion
- Market development
- Product innovation
- Pricing optimization
2. **Margin Improvement**
- Operational efficiency
- Cost reduction
- Mix optimization
- Pricing power
3. **Multiple Expansion**
- Market positioning
- Growth trajectory
- Risk reduction
- Story telling
FILE:ceo-advisor/references/leadership_organizational_culture.md
# Leadership & Organizational Culture Guide
## Leadership Philosophy
### The Five Dimensions of CEO Leadership
1. **Visionary Leadership**
- Define compelling future state
- Communicate vision consistently
- Inspire action toward vision
- Measure progress systematically
2. **Strategic Leadership**
- Set clear priorities
- Allocate resources optimally
- Make tough trade-offs
- Drive execution excellence
3. **Operational Leadership**
- Establish performance standards
- Build scalable systems
- Drive continuous improvement
- Ensure accountability
4. **People Leadership**
- Attract top talent
- Develop future leaders
- Foster engagement
- Build inclusive culture
5. **External Leadership**
- Represent company publicly
- Build strategic partnerships
- Engage stakeholders effectively
- Shape industry direction
## Organizational Culture Framework
### Culture Definition & Assessment
#### Cultural Dimensions Model
**Innovation ← → Stability**
- Risk tolerance level
- Change readiness
- Experimentation mindset
- Learning from failure
**Competition ← → Collaboration**
- Internal dynamics
- Knowledge sharing
- Team vs individual rewards
- Cross-functional cooperation
**Customer ← → Operations**
- External vs internal focus
- Customer centricity
- Process emphasis
- Quality standards
**Short-term ← → Long-term**
- Planning horizons
- Investment philosophy
- Performance metrics
- Stakeholder balance
### Culture Transformation Roadmap
#### Phase 1: Assessment (Months 1-2)
**Current State Analysis**:
- Employee survey (engagement, values alignment)
- Culture assessment (competing values framework)
- Leadership 360 feedback
- Exit interview analysis
- Customer feedback integration
**Gap Analysis**:
- Current vs desired culture
- Behavioral gaps
- System misalignments
- Leadership gaps
- Communication gaps
#### Phase 2: Design (Months 2-3)
**Target Culture Definition**:
- Core values articulation
- Behavioral standards
- Leadership principles
- Decision principles
- Performance expectations
**Change Strategy**:
- Stakeholder mapping
- Communication plan
- Training requirements
- System changes needed
- Quick wins identification
#### Phase 3: Implementation (Months 4-12)
**Launch Activities**:
- Leadership alignment sessions
- All-hands kickoff
- Values workshops
- Behavioral training
- System updates
**Reinforcement Mechanisms**:
- Recognition programs
- Performance integration
- Hiring/promotion criteria
- Story collection
- Celebration events
#### Phase 4: Embedding (Months 12+)
**Sustainability Actions**:
- Regular pulse surveys
- Culture champions network
- Continuous reinforcement
- System alignment
- Leadership modeling
## Leadership Development
### Executive Team Development
#### Team Effectiveness Model
**Foundation Elements**:
1. **Trust** - Vulnerability-based trust
2. **Conflict** - Healthy debate
3. **Commitment** - Buy-in to decisions
4. **Accountability** - Peer accountability
5. **Results** - Collective outcomes
#### Executive Team Charter
```
Our Executive Team Charter
Purpose:
Lead [Company] to achieve its vision of [Vision Statement]
Responsibilities:
• Set strategic direction
• Allocate resources
• Drive performance
• Develop talent
• Shape culture
Operating Principles:
• Debate in private, unite in public
• Challenge ideas, support people
• Company first, function second
• Transparency with trust
• Accountability without blame
Meeting Cadence:
• Weekly tactical (2 hours)
• Monthly strategic (4 hours)
• Quarterly offsite (2 days)
• Annual planning (3 days)
Decision Rights:
• CEO: Final decision after consultation
• Consensus: Strategic initiatives
• Individual: Functional operations
• Escalation: Board-level matters
Success Metrics:
• Company performance vs plan
• Employee engagement score
• Customer satisfaction (NPS)
• Team effectiveness rating
```
### Succession Planning
#### Succession Planning Framework
**CEO Succession Timeline**:
**Ongoing**:
- Identify potential successors
- Development plan execution
- Board exposure
- External benchmarking
**T-3 Years**:
- Formal succession planning
- Candidate assessment
- Development acceleration
- Emergency plan update
**T-1 Year**:
- Final candidate selection
- Transition planning
- Communication strategy
- Onboarding preparation
**Transition**:
- Announcement
- Knowledge transfer
- Stakeholder introductions
- Gradual handover
#### Talent Pipeline Development
**9-Box Grid for Talent Review**:
```
Performance →
↑
│ Rising │ High │ Star
High│ Star │Performer│ Performer
├─────────┼─────────┼──────────
│Solid │ Core │ High
Med │Performer│Performer│ Potential
├─────────┼─────────┼──────────
│ Under │Inconsist│ New/
Low │Performer│ -ent │ Learning
└─────────┴─────────┴──────────
Low Medium High
Potential →
```
**Development Strategies by Box**:
- **Stars**: Accelerated development, stretch assignments
- **High Performers**: Retention focus, leadership opportunities
- **High Potentials**: Intensive coaching, skill building
- **Core Performers**: Engagement, incremental growth
- **Underperformers**: Performance improvement or exit
### Leadership Competency Model
#### Core Leadership Competencies
**Strategic Thinking**
- Vision development
- Systems thinking
- Innovation mindset
- External awareness
- Long-term planning
**Execution Excellence**
- Results orientation
- Decision quality
- Problem solving
- Process management
- Risk management
**People Leadership**
- Team building
- Talent development
- Communication
- Influence
- Emotional intelligence
**Personal Excellence**
- Integrity
- Resilience
- Continuous learning
- Self-awareness
- Adaptability
## Communication & Engagement
### Internal Communication Strategy
#### Communication Channels
| Channel | Frequency | Purpose | Audience |
|---------|-----------|---------|----------|
| All-hands meeting | Monthly | Updates, Q&A | All employees |
| Leadership cascade | Weekly | Alignment | Managers |
| CEO email | Bi-weekly | Vision, recognition | All employees |
| Town halls | Quarterly | Deep dives | All employees |
| Skip-levels | Monthly | Direct feedback | Various levels |
| Intranet | Daily | News, resources | All employees |
| Slack/Teams | Real-time | Collaboration | All employees |
#### CEO Communication Calendar
**Weekly**:
- Executive team meeting
- Leadership message cascade
- Customer/partner touchpoint
**Bi-weekly**:
- Company-wide email
- Skip-level meetings
- Media/analyst interaction
**Monthly**:
- All-hands meeting
- Board member touchpoint
- Employee roundtable
**Quarterly**:
- Earnings communication
- Town hall deep-dive
- Strategy review
- Culture celebration
### Employee Engagement
#### Engagement Survey Framework
**Dimensions Measured**:
1. Purpose & Vision (alignment, inspiration)
2. Leadership (trust, communication)
3. Management (support, development)
4. Work Environment (tools, processes)
5. Growth (career, learning)
6. Recognition (appreciation, fairness)
7. Wellbeing (balance, benefits)
8. Belonging (inclusion, connection)
**Action Planning Process**:
1. Share results transparently
2. Identify 2-3 focus areas
3. Create action teams
4. Define success metrics
5. Implement changes
6. Communicate progress
7. Measure impact
#### Engagement Initiatives
**Recognition Programs**:
- Spot awards (peer-nominated)
- Quarterly achievements
- Annual excellence awards
- Values champions
- Innovation celebrations
- Customer hero awards
**Development Programs**:
- Leadership academy
- Mentorship program
- Rotation opportunities
- Tuition reimbursement
- Conference attendance
- Skill workshops
**Wellbeing Initiatives**:
- Flexible work arrangements
- Mental health support
- Wellness programs
- Time-off policies
- Family support
- Financial wellness
## Performance Management
### OKR Framework
#### OKR Setting Process
**Company OKRs** (Annual)
↓
**Department OKRs** (Quarterly)
↓
**Team OKRs** (Quarterly)
↓
**Individual OKRs** (Quarterly)
#### OKR Template
**Objective**: [Qualitative, inspirational goal]
**Key Results**:
1. [Quantitative outcome] from [X] to [Y]
2. [Quantitative outcome] from [X] to [Y]
3. [Quantitative outcome] from [X] to [Y]
**Example**:
```
Objective: Become the market leader in customer satisfaction
Key Results:
1. Increase NPS from 45 to 70
2. Reduce support ticket resolution from 48h to 24h
3. Achieve 95% customer retention rate (from 87%)
```
### Performance Review System
#### Continuous Performance Management
**Weekly**: 1-on-1 check-ins (30 min)
- Progress on priorities
- Obstacles/support needed
- Feedback exchange
- Next week focus
**Monthly**: Development discussion (60 min)
- Skill development
- Career aspirations
- Stretch opportunities
- Learning plan
**Quarterly**: Performance review (90 min)
- OKR assessment
- Competency evaluation
- 360 feedback review
- Development planning
**Annual**: Compensation review
- Performance rating
- Compensation adjustment
- Promotion decisions
- Succession planning
## Change Management
### Change Leadership Model
#### Eight-Step Change Process
1. **Create Urgency**
- Share compelling data
- Highlight risks of status quo
- Create dissatisfaction with current state
2. **Build Coalition**
- Identify change champions
- Ensure executive alignment
- Engage influential supporters
3. **Form Vision**
- Define clear end state
- Create inspiring narrative
- Develop strategy
4. **Communicate Vision**
- Multi-channel communication
- Repetition and consistency
- Two-way dialogue
5. **Empower Action**
- Remove barriers
- Change systems/processes
- Encourage risk-taking
6. **Create Quick Wins**
- Identify early victories
- Celebrate visibly
- Build momentum
7. **Consolidate Gains**
- Don't declare victory early
- Continue driving change
- Address deeper issues
8. **Anchor in Culture**
- Reinforce through systems
- Celebrate new behaviors
- Ensure leadership continuity
### Organizational Design
#### Design Principles
**Customer-Centric**
- Organize around customer needs
- Minimize handoffs
- Clear ownership
- Fast decision-making
**Scalable**
- Consistent structures
- Clear roles/responsibilities
- Repeatable processes
- Growth-ready
**Agile**
- Cross-functional teams
- Rapid iteration
- Continuous learning
- Adaptive planning
**Efficient**
- Appropriate spans of control (5-7)
- Minimal layers (max 5-6)
- Clear decision rights
- Eliminated redundancy
#### Reorganization Playbook
**Pre-announcement** (4-6 weeks)
- Design new structure
- Identify leadership
- Plan communication
- Prepare materials
**Announcement** (Day 0)
- All-hands meeting
- Written communication
- Q&A sessions
- Manager toolkit
**Transition** (30 days)
- Role clarifications
- Team formations
- Process updates
- System changes
**Stabilization** (60-90 days)
- Monitor progress
- Address issues
- Refine as needed
- Celebrate success
## Crisis Leadership
### Crisis Response Framework
#### Leadership During Crisis
**Immediate Response** (0-24 hours)
- Establish command center
- Assess situation
- Communicate frequently
- Make rapid decisions
- Show visible leadership
**Stabilization** (1-7 days)
- Implement solutions
- Maintain communication
- Support teams
- Monitor progress
- Adjust approach
**Recovery** (1-4 weeks)
- Execute recovery plan
- Address long-term impacts
- Learn from crisis
- Strengthen resilience
- Recognize heroes
#### Crisis Communication
**Internal Communication**:
- Frequency: 2x daily minimum
- Channels: Email, video, town halls
- Content: Facts, actions, support
- Tone: Calm, confident, caring
**External Communication**:
- Stakeholders: Customers, partners, investors, media
- Frequency: As needed
- Channels: Website, press, social
- Content: Impact, response, timeline
- Tone: Transparent, responsible
## Innovation Culture
### Innovation Framework
#### Innovation Portfolio
**Horizon 1** (70% resources)
- Core business innovation
- Incremental improvements
- 6-18 month timeline
- Lower risk
**Horizon 2** (20% resources)
- Emerging opportunities
- Adjacent markets
- 18-36 month timeline
- Moderate risk
**Horizon 3** (10% resources)
- Transformational bets
- New business models
- 3-5 year timeline
- Higher risk
#### Innovation Programs
**Innovation Time**
- 20% time for projects
- Hackathons quarterly
- Innovation challenges
- Idea platforms
- Patent incentives
**Innovation Metrics**
- % revenue from new products
- Ideas generated/implemented
- Time to market
- Innovation ROI
- Patent applications
## Diversity, Equity & Inclusion
### DEI Strategy Framework
#### Four Pillars of DEI
1. **Representation**
- Diverse hiring
- Promotion equity
- Leadership diversity
- Board diversity
2. **Inclusion**
- Belonging index
- Psychological safety
- Equitable practices
- Bias mitigation
3. **Development**
- Sponsorship programs
- ERG support
- Leadership development
- Career pathways
4. **Accountability**
- DEI metrics
- Leader goals
- Regular reporting
- Transparency
#### DEI Metrics Dashboard
| Metric | Current | Target | Timeline |
|--------|---------|--------|----------|
| Women in leadership | X% | Y% | Z years |
| Ethnic diversity | X% | Y% | Z years |
| Pay equity gap | X% | 0% | Z years |
| Inclusion index | X/100 | Y/100 | Z years |
| Retention equality | X% diff | 0% diff | Z years |
## Executive Presence
### CEO Personal Brand
#### Brand Elements
**Vision**: What future you're creating
**Values**: What you stand for
**Voice**: How you communicate
**Visibility**: Where you show up
**Value**: What you deliver
#### Executive Communication
**Speaking Frameworks**:
**PREP Method**:
- **P**oint: Main message
- **R**eason: Why it matters
- **E**xample: Concrete illustration
- **P**oint: Restate message
**STAR Method** (for stories):
- **S**ituation: Context
- **T**ask: Challenge
- **A**ction: What was done
- **R**esult: Outcome
#### Media Training Essentials
**Key Message Discipline**:
- 3 key messages maximum
- Bridge to messages
- Sound bites ready
- Avoid speculation
- Stay on record
**Interview Techniques**:
- Pause before answering
- Bridge to key messages
- Use examples/stories
- Maintain eye contact
- Control pace
FILE:ceo-advisor/scripts/financial_scenario_analyzer.py
#!/usr/bin/env python3
"""
Financial Scenario Analyzer - Model different business scenarios and their financial impact
"""
import json
from typing import Dict, List, Tuple
import math
class FinancialScenarioAnalyzer:
def __init__(self):
self.key_metrics = [
'revenue', 'gross_margin', 'operating_expenses',
'ebitda', 'cash_flow', 'runway', 'valuation'
]
self.growth_models = {
'linear': lambda base, rate, period: base * (1 + rate * period),
'exponential': lambda base, rate, period: base * math.pow(1 + rate, period),
'logarithmic': lambda base, rate, period: base * (1 + rate * math.log(period + 1)),
's_curve': lambda base, rate, period: base * (2 / (1 + math.exp(-rate * period)))
}
def analyze_scenarios(self, base_case: Dict, scenarios: List[Dict]) -> Dict:
"""Analyze multiple financial scenarios"""
results = {
'base_case_summary': self._summarize_financials(base_case),
'scenario_analysis': [],
'sensitivity_analysis': {},
'recommendation': {},
'risk_adjusted_view': {}
}
# Analyze each scenario
for scenario in scenarios:
scenario_result = self._analyze_scenario(base_case, scenario)
results['scenario_analysis'].append(scenario_result)
# Sensitivity analysis
results['sensitivity_analysis'] = self._perform_sensitivity_analysis(
base_case,
scenarios
)
# Risk-adjusted view
results['risk_adjusted_view'] = self._calculate_risk_adjusted_returns(
results['scenario_analysis']
)
# Generate recommendation
results['recommendation'] = self._generate_recommendation(
results['scenario_analysis'],
results['risk_adjusted_view']
)
return results
def _summarize_financials(self, financials: Dict) -> Dict:
"""Summarize key financial metrics"""
revenue = financials.get('revenue', 0)
cogs = financials.get('cogs', 0)
opex = financials.get('operating_expenses', 0)
gross_profit = revenue - cogs
gross_margin = (gross_profit / revenue * 100) if revenue > 0 else 0
ebitda = gross_profit - opex
ebitda_margin = (ebitda / revenue * 100) if revenue > 0 else 0
return {
'revenue': revenue,
'gross_profit': gross_profit,
'gross_margin': gross_margin,
'operating_expenses': opex,
'ebitda': ebitda,
'ebitda_margin': ebitda_margin,
'cash': financials.get('cash', 0),
'burn_rate': financials.get('burn_rate', 0),
'runway_months': self._calculate_runway(
financials.get('cash', 0),
financials.get('burn_rate', 0)
)
}
def _calculate_runway(self, cash: float, burn_rate: float) -> float:
"""Calculate months of runway"""
if burn_rate <= 0:
return float('inf')
return cash / burn_rate
def _analyze_scenario(self, base_case: Dict, scenario: Dict) -> Dict:
"""Analyze a single scenario"""
name = scenario.get('name', 'Unnamed Scenario')
probability = scenario.get('probability', 0.5)
# Apply scenario changes
projected_financials = self._apply_scenario_changes(base_case, scenario)
# Calculate metrics for each year
projections = []
current_state = projected_financials.copy()
for year in range(1, 4): # 3-year projection
year_projection = self._project_year(
current_state,
scenario,
year
)
projections.append(year_projection)
current_state = year_projection
# Calculate NPV and IRR
cash_flows = [p['free_cash_flow'] for p in projections]
npv = self._calculate_npv(cash_flows, scenario.get('discount_rate', 0.1))
irr = self._calculate_irr(cash_flows, base_case.get('initial_investment', 0))
return {
'name': name,
'probability': probability,
'projections': projections,
'npv': npv,
'irr': irr,
'break_even_month': self._find_break_even(projections),
'total_return': self._calculate_total_return(projections, base_case),
'key_assumptions': scenario.get('assumptions', [])
}
def _apply_scenario_changes(self, base_case: Dict, scenario: Dict) -> Dict:
"""Apply scenario changes to base case"""
result = base_case.copy()
changes = scenario.get('changes', {})
for key, change in changes.items():
if key in result:
if isinstance(change, dict):
# Relative change
if 'multiply' in change:
result[key] *= change['multiply']
elif 'add' in change:
result[key] += change['add']
else:
# Absolute change
result[key] = change
return result
def _project_year(self, current_state: Dict, scenario: Dict, year: int) -> Dict:
"""Project financials for a specific year"""
growth_model = scenario.get('growth_model', 'exponential')
growth_rate = scenario.get('growth_rate', 0.3)
# Apply growth model
model_func = self.growth_models.get(growth_model, self.growth_models['linear'])
revenue = model_func(
current_state.get('revenue', 0),
growth_rate,
year
)
# Scale other metrics
cogs = revenue * scenario.get('cogs_ratio', 0.3)
opex = current_state.get('operating_expenses', 0) * (1 + scenario.get('opex_growth', 0.15))
gross_profit = revenue - cogs
ebitda = gross_profit - opex
# Calculate free cash flow (simplified)
capex = revenue * scenario.get('capex_ratio', 0.05)
working_capital_change = (revenue - current_state.get('revenue', 0)) * 0.1
free_cash_flow = ebitda - capex - working_capital_change
return {
'year': year,
'revenue': revenue,
'gross_profit': gross_profit,
'gross_margin': (gross_profit / revenue * 100) if revenue > 0 else 0,
'operating_expenses': opex,
'ebitda': ebitda,
'ebitda_margin': (ebitda / revenue * 100) if revenue > 0 else 0,
'free_cash_flow': free_cash_flow,
'cumulative_cash_flow': current_state.get('cumulative_cash_flow', 0) + free_cash_flow
}
def _calculate_npv(self, cash_flows: List[float], discount_rate: float) -> float:
"""Calculate Net Present Value"""
npv = 0
for i, cf in enumerate(cash_flows):
npv += cf / math.pow(1 + discount_rate, i + 1)
return npv
def _calculate_irr(self, cash_flows: List[float], initial_investment: float) -> float:
"""Calculate Internal Rate of Return (simplified)"""
if not cash_flows or initial_investment == 0:
return 0
# Simple IRR approximation
total_return = sum(cash_flows)
years = len(cash_flows)
if initial_investment > 0:
return math.pow(total_return / initial_investment, 1/years) - 1
return 0
def _find_break_even(self, projections: List[Dict]) -> int:
"""Find break-even month"""
months = 0
for projection in projections:
months += 12
if projection.get('ebitda', 0) > 0:
# Interpolate to find exact month
if months == 12:
return months
prev_ebitda = projections[projection['year']-2].get('ebitda', 0) if projection['year'] > 1 else 0
monthly_improvement = (projection['ebitda'] - prev_ebitda) / 12
if monthly_improvement > 0:
months_to_breakeven = abs(prev_ebitda) / monthly_improvement
return int(months - 12 + months_to_breakeven)
return -1 # Not reached
def _calculate_total_return(self, projections: List[Dict], base_case: Dict) -> float:
"""Calculate total return multiple"""
initial = base_case.get('valuation', 1000000)
# Simple valuation at end (10x revenue multiple for SaaS)
final_revenue = projections[-1]['revenue'] if projections else 0
final_valuation = final_revenue * 10
return (final_valuation / initial) if initial > 0 else 0
def _perform_sensitivity_analysis(self, base_case: Dict, scenarios: List[Dict]) -> Dict:
"""Perform sensitivity analysis on key variables"""
sensitivity = {}
key_variables = ['growth_rate', 'gross_margin', 'customer_acquisition_cost']
for variable in key_variables:
sensitivity[variable] = {
'low': self._calculate_variable_impact(base_case, variable, -0.2),
'base': self._calculate_variable_impact(base_case, variable, 0),
'high': self._calculate_variable_impact(base_case, variable, 0.2)
}
return sensitivity
def _calculate_variable_impact(self, base_case: Dict, variable: str, change: float) -> float:
"""Calculate impact of variable change on valuation"""
# Simplified impact calculation
impacts = {
'growth_rate': 2.5, # 2.5x multiplier on valuation
'gross_margin': 1.8, # 1.8x multiplier
'customer_acquisition_cost': -1.2 # Negative impact
}
base_value = 10000000 # Base valuation
impact_multiplier = impacts.get(variable, 1.0)
return base_value * (1 + change * impact_multiplier)
def _calculate_risk_adjusted_returns(self, scenarios: List[Dict]) -> Dict:
"""Calculate risk-adjusted returns"""
expected_value = 0
best_case = None
worst_case = None
for scenario in scenarios:
probability = scenario['probability']
npv = scenario['npv']
expected_value += probability * npv
if best_case is None or npv > best_case['npv']:
best_case = scenario
if worst_case is None or npv < worst_case['npv']:
worst_case = scenario
# Calculate standard deviation (simplified)
variance = sum([
scenario['probability'] * math.pow(scenario['npv'] - expected_value, 2)
for scenario in scenarios
])
std_dev = math.sqrt(variance)
return {
'expected_value': expected_value,
'best_case': best_case['name'] if best_case else 'None',
'best_case_npv': best_case['npv'] if best_case else 0,
'worst_case': worst_case['name'] if worst_case else 'None',
'worst_case_npv': worst_case['npv'] if worst_case else 0,
'standard_deviation': std_dev,
'sharpe_ratio': (expected_value / std_dev) if std_dev > 0 else 0
}
def _generate_recommendation(self, scenarios: List[Dict], risk_adjusted: Dict) -> Dict:
"""Generate recommendation based on analysis"""
recommendation = {
'recommended_scenario': '',
'rationale': [],
'key_actions': [],
'risk_mitigation': []
}
# Find optimal scenario
best_risk_adjusted = max(scenarios, key=lambda s: s['npv'] * s['probability'])
recommendation['recommended_scenario'] = best_risk_adjusted['name']
# Generate rationale
if best_risk_adjusted['npv'] > 0:
recommendation['rationale'].append(f"Positive NPV of ,.0f")
if best_risk_adjusted['irr'] > 0.15:
recommendation['rationale'].append(f"Strong IRR of {best_risk_adjusted['irr']:.1%}")
if best_risk_adjusted['break_even_month'] > 0 and best_risk_adjusted['break_even_month'] < 24:
recommendation['rationale'].append(f"Quick path to profitability ({best_risk_adjusted['break_even_month']} months)")
# Key actions
recommendation['key_actions'] = [
'Secure funding for growth initiatives',
'Build scalable operational infrastructure',
'Invest in customer acquisition channels',
'Strengthen unit economics',
'Establish financial controls'
]
# Risk mitigation
if risk_adjusted['standard_deviation'] > risk_adjusted['expected_value'] * 0.5:
recommendation['risk_mitigation'].append('High variability - consider hedging strategies')
recommendation['risk_mitigation'].extend([
'Maintain 12+ months runway',
'Diversify revenue streams',
'Build contingency plans for downside scenarios'
])
return recommendation
def analyze_financial_scenarios(base_case: Dict, scenarios: List[Dict]) -> str:
"""Main function to analyze financial scenarios"""
analyzer = FinancialScenarioAnalyzer()
results = analyzer.analyze_scenarios(base_case, scenarios)
# Format output
output = [
"=== Financial Scenario Analysis ===",
"",
"Base Case Summary:",
f" Revenue: ,.0f",
f" Gross Margin: {results['base_case_summary']['gross_margin']:.1f}%",
f" EBITDA: ,.0f",
f" Runway: {results['base_case_summary']['runway_months']:.1f} months",
"",
"Scenario Analysis:"
]
for scenario in results['scenario_analysis']:
output.append(f"\n{scenario['name']} (Probability: {scenario['probability']:.0%})")
output.append(f" NPV: ,.0f")
output.append(f" IRR: {scenario['irr']:.1%}")
output.append(f" Break-even: {scenario['break_even_month']} months")
output.append(f" Return Multiple: {scenario['total_return']:.1f}x")
# Show Year 3 projection
if scenario['projections']:
year3 = scenario['projections'][-1]
output.append(f" Year 3 Revenue: ,.0f")
output.append(f" Year 3 EBITDA Margin: {year3['ebitda_margin']:.1f}%")
output.extend([
"",
"Risk-Adjusted Analysis:",
f" Expected Value: ,.0f",
f" Best Case: {results['risk_adjusted_view']['best_case']} (,.0f)",
f" Worst Case: {results['risk_adjusted_view']['worst_case']} (,.0f)",
f" Risk (Std Dev): ,.0f",
f" Sharpe Ratio: {results['risk_adjusted_view']['sharpe_ratio']:.2f}",
"",
f"RECOMMENDATION: {results['recommendation']['recommended_scenario']}",
"",
"Rationale:"
])
for reason in results['recommendation']['rationale']:
output.append(f" • {reason}")
output.extend([
"",
"Key Actions:"
])
for action in results['recommendation']['key_actions'][:3]:
output.append(f" • {action}")
return '\n'.join(output)
if __name__ == "__main__":
# Example usage
example_base_case = {
'revenue': 5000000,
'cogs': 1500000,
'operating_expenses': 3000000,
'cash': 2000000,
'burn_rate': 200000,
'valuation': 20000000,
'initial_investment': 5000000
}
example_scenarios = [
{
'name': 'Aggressive Growth',
'probability': 0.3,
'growth_model': 'exponential',
'growth_rate': 0.5,
'changes': {
'operating_expenses': {'multiply': 1.3}
},
'assumptions': ['Market expansion successful', 'Product-market fit achieved'],
'cogs_ratio': 0.25,
'opex_growth': 0.3,
'capex_ratio': 0.08,
'discount_rate': 0.12
},
{
'name': 'Moderate Growth',
'probability': 0.5,
'growth_model': 'exponential',
'growth_rate': 0.3,
'changes': {},
'assumptions': ['Steady market growth', 'Competition remains stable'],
'cogs_ratio': 0.3,
'opex_growth': 0.15,
'capex_ratio': 0.05,
'discount_rate': 0.10
},
{
'name': 'Conservative',
'probability': 0.2,
'growth_model': 'linear',
'growth_rate': 0.15,
'changes': {
'operating_expenses': {'multiply': 0.9}
},
'assumptions': ['Market headwinds', 'Focus on profitability'],
'cogs_ratio': 0.35,
'opex_growth': 0.05,
'capex_ratio': 0.03,
'discount_rate': 0.08
}
]
print(analyze_financial_scenarios(example_base_case, example_scenarios))
FILE:ceo-advisor/scripts/strategy_analyzer.py
#!/usr/bin/env python3
"""
Strategic Planning Analyzer - Comprehensive business strategy assessment tool
"""
import json
from typing import Dict, List, Tuple
from datetime import datetime, timedelta
import math
class StrategyAnalyzer:
def __init__(self):
self.strategic_pillars = {
'market_position': {
'weight': 0.25,
'factors': ['market_share', 'brand_strength', 'competitive_advantage', 'customer_loyalty']
},
'financial_health': {
'weight': 0.25,
'factors': ['revenue_growth', 'profitability', 'cash_flow', 'unit_economics']
},
'operational_excellence': {
'weight': 0.20,
'factors': ['efficiency', 'quality', 'scalability', 'innovation']
},
'organizational_capability': {
'weight': 0.20,
'factors': ['talent', 'culture', 'leadership', 'agility']
},
'growth_potential': {
'weight': 0.10,
'factors': ['market_size', 'expansion_opportunities', 'product_pipeline', 'partnerships']
}
}
self.strategic_frameworks = {
'porter_five_forces': [
'competitive_rivalry',
'supplier_power',
'buyer_power',
'threat_of_substitution',
'threat_of_new_entry'
],
'swot': ['strengths', 'weaknesses', 'opportunities', 'threats'],
'bcg_matrix': ['stars', 'cash_cows', 'question_marks', 'dogs'],
'ansoff_matrix': ['market_penetration', 'market_development', 'product_development', 'diversification']
}
def analyze_strategic_position(self, company_data: Dict) -> Dict:
"""Comprehensive strategic analysis"""
results = {
'timestamp': datetime.now().isoformat(),
'company': company_data.get('name', 'Company'),
'strategic_health_score': 0,
'pillar_analysis': {},
'framework_analysis': {},
'strategic_options': [],
'risk_assessment': {},
'recommendations': [],
'roadmap': {}
}
# Analyze strategic pillars
total_score = 0
for pillar, config in self.strategic_pillars.items():
pillar_score = self._analyze_pillar(
company_data.get(pillar, {}),
config['factors']
)
weighted_score = pillar_score * config['weight']
results['pillar_analysis'][pillar] = {
'score': pillar_score,
'weighted_score': weighted_score,
'level': self._get_level(pillar_score),
'factors': self._get_pillar_details(company_data.get(pillar, {}), config['factors'])
}
total_score += weighted_score
results['strategic_health_score'] = round(total_score, 1)
# Framework analysis
results['framework_analysis'] = self._apply_frameworks(company_data)
# Generate strategic options
results['strategic_options'] = self._generate_strategic_options(
results['pillar_analysis'],
company_data.get('context', {})
)
# Risk assessment
results['risk_assessment'] = self._assess_strategic_risks(
company_data,
results['strategic_options']
)
# Generate roadmap
results['roadmap'] = self._create_strategic_roadmap(
results['strategic_options'],
company_data.get('timeline', 12)
)
# Generate recommendations
results['recommendations'] = self._generate_recommendations(results)
return results
def _analyze_pillar(self, pillar_data: Dict, factors: List) -> float:
"""Analyze a strategic pillar"""
if not pillar_data:
return 50.0
total_score = 0
count = 0
for factor in factors:
if factor in pillar_data:
score = pillar_data[factor]
total_score += score
count += 1
return (total_score / count) if count > 0 else 50.0
def _get_pillar_details(self, pillar_data: Dict, factors: List) -> List[Dict]:
"""Get detailed factor analysis"""
details = []
for factor in factors:
score = pillar_data.get(factor, 50)
details.append({
'factor': factor.replace('_', ' ').title(),
'score': score,
'status': 'Strong' if score >= 70 else 'Adequate' if score >= 40 else 'Weak'
})
return details
def _get_level(self, score: float) -> str:
"""Convert score to level"""
if score >= 80:
return 'Excellent'
elif score >= 70:
return 'Strong'
elif score >= 50:
return 'Adequate'
elif score >= 30:
return 'Weak'
else:
return 'Critical'
def _apply_frameworks(self, company_data: Dict) -> Dict:
"""Apply strategic frameworks"""
frameworks = {}
# SWOT Analysis
swot_data = company_data.get('swot', {})
frameworks['swot'] = {
'strengths': swot_data.get('strengths', [
'Strong brand recognition',
'Experienced leadership team',
'Robust technology platform'
]),
'weaknesses': swot_data.get('weaknesses', [
'Limited geographic presence',
'High customer acquisition cost',
'Technical debt'
]),
'opportunities': swot_data.get('opportunities', [
'Growing market demand',
'M&A opportunities',
'New product categories'
]),
'threats': swot_data.get('threats', [
'Increasing competition',
'Regulatory changes',
'Economic uncertainty'
])
}
# Porter's Five Forces
forces = company_data.get('competitive_forces', {})
frameworks['porter_analysis'] = {
'competitive_rivalry': forces.get('rivalry', 70),
'supplier_power': forces.get('suppliers', 40),
'buyer_power': forces.get('buyers', 60),
'threat_of_substitutes': forces.get('substitutes', 50),
'threat_of_new_entrants': forces.get('new_entrants', 45),
'overall_attractiveness': self._calculate_industry_attractiveness(forces)
}
# BCG Matrix for product portfolio
products = company_data.get('products', [])
frameworks['portfolio_analysis'] = self._analyze_portfolio(products)
return frameworks
def _calculate_industry_attractiveness(self, forces: Dict) -> float:
"""Calculate industry attractiveness from Porter's forces"""
# Lower forces = more attractive industry
rivalry = 100 - forces.get('rivalry', 50)
supplier = 100 - forces.get('suppliers', 50)
buyer = 100 - forces.get('buyers', 50)
substitutes = 100 - forces.get('substitutes', 50)
new_entrants = 100 - forces.get('new_entrants', 50)
avg = (rivalry + supplier + buyer + substitutes + new_entrants) / 5
return round(avg, 1)
def _analyze_portfolio(self, products: List) -> Dict:
"""Analyze product portfolio using BCG matrix"""
portfolio = {
'stars': [],
'cash_cows': [],
'question_marks': [],
'dogs': []
}
for product in products:
growth = product.get('market_growth', 0)
share = product.get('market_share', 0)
if growth > 10 and share > 50:
portfolio['stars'].append(product.get('name', 'Product'))
elif growth <= 10 and share > 50:
portfolio['cash_cows'].append(product.get('name', 'Product'))
elif growth > 10 and share <= 50:
portfolio['question_marks'].append(product.get('name', 'Product'))
else:
portfolio['dogs'].append(product.get('name', 'Product'))
return portfolio
def _generate_strategic_options(self, pillar_analysis: Dict, context: Dict) -> List[Dict]:
"""Generate strategic options based on analysis"""
options = []
# Check market position
market_score = pillar_analysis['market_position']['score']
if market_score < 60:
options.append({
'name': 'Market Leadership Initiative',
'type': 'market_penetration',
'description': 'Aggressive market share capture through competitive pricing and marketing',
'investment': 'High',
'timeframe': '12-18 months',
'expected_impact': 'Increase market share by 10-15%',
'priority': 9
})
# Check financial health
financial_score = pillar_analysis['financial_health']['score']
if financial_score < 50:
options.append({
'name': 'Profitability Turnaround',
'type': 'operational_excellence',
'description': 'Cost reduction and revenue optimization program',
'investment': 'Medium',
'timeframe': '6-9 months',
'expected_impact': 'Improve margins by 5-8%',
'priority': 10
})
# Check growth potential
growth_score = pillar_analysis['growth_potential']['score']
if growth_score > 70:
options.append({
'name': 'Expansion Strategy',
'type': 'market_development',
'description': 'Enter new geographic markets or customer segments',
'investment': 'High',
'timeframe': '18-24 months',
'expected_impact': 'Revenue growth of 30-40%',
'priority': 8
})
# Innovation opportunities
if context.get('industry_disruption', False):
options.append({
'name': 'Digital Transformation',
'type': 'innovation',
'description': 'Comprehensive digitalization of business processes and customer experience',
'investment': 'Very High',
'timeframe': '24-36 months',
'expected_impact': 'Future-proof business model',
'priority': 9
})
# M&A opportunities
if context.get('cash_available', 0) > 100000000:
options.append({
'name': 'Strategic Acquisition',
'type': 'acquisition',
'description': 'Acquire complementary businesses or competitors',
'investment': 'Very High',
'timeframe': '6-12 months',
'expected_impact': 'Instant scale and capability',
'priority': 7
})
# Sort by priority
options.sort(key=lambda x: x['priority'], reverse=True)
return options[:5] # Top 5 strategic options
def _assess_strategic_risks(self, company_data: Dict, strategic_options: List) -> Dict:
"""Assess strategic risks"""
risks = {
'execution_risk': self._calculate_execution_risk(company_data),
'market_risk': self._calculate_market_risk(company_data),
'financial_risk': self._calculate_financial_risk(company_data),
'competitive_risk': self._calculate_competitive_risk(company_data),
'regulatory_risk': company_data.get('regulatory_risk', 30),
'overall_risk': 0,
'mitigation_strategies': []
}
# Calculate overall risk
risk_values = [
risks['execution_risk'],
risks['market_risk'],
risks['financial_risk'],
risks['competitive_risk'],
risks['regulatory_risk']
]
risks['overall_risk'] = sum(risk_values) / len(risk_values)
# Generate mitigation strategies
if risks['execution_risk'] > 60:
risks['mitigation_strategies'].append({
'risk': 'Execution',
'strategy': 'Strengthen PMO, hire experienced executives, implement OKRs'
})
if risks['market_risk'] > 60:
risks['mitigation_strategies'].append({
'risk': 'Market',
'strategy': 'Diversify revenue streams, build strategic partnerships'
})
if risks['financial_risk'] > 60:
risks['mitigation_strategies'].append({
'risk': 'Financial',
'strategy': 'Improve cash management, secure credit facilities, optimize working capital'
})
return risks
def _calculate_execution_risk(self, data: Dict) -> float:
"""Calculate execution risk"""
org_capability = data.get('organizational_capability', {})
factors = [
100 - org_capability.get('leadership', 50),
100 - org_capability.get('talent', 50),
100 - org_capability.get('agility', 50),
data.get('complexity_score', 50)
]
return sum(factors) / len(factors)
def _calculate_market_risk(self, data: Dict) -> float:
"""Calculate market risk"""
market = data.get('market_position', {})
factors = [
100 - market.get('market_share', 50),
data.get('market_volatility', 50),
data.get('customer_concentration', 50)
]
return sum(factors) / len(factors)
def _calculate_financial_risk(self, data: Dict) -> float:
"""Calculate financial risk"""
financial = data.get('financial_health', {})
factors = [
100 - financial.get('cash_flow', 50),
100 - financial.get('profitability', 50),
data.get('debt_ratio', 50),
data.get('burn_rate', 50) if 'burn_rate' in data else 30
]
return sum(factors) / len(factors)
def _calculate_competitive_risk(self, data: Dict) -> float:
"""Calculate competitive risk"""
forces = data.get('competitive_forces', {})
return (forces.get('rivalry', 50) + forces.get('new_entrants', 50)) / 2
def _create_strategic_roadmap(self, options: List, timeline_months: int) -> Dict:
"""Create implementation roadmap"""
roadmap = {
'phases': [],
'milestones': [],
'resource_requirements': {},
'success_metrics': []
}
# Define phases
phases = [
{
'phase': 'Foundation',
'months': '0-3',
'focus': 'Build capabilities and quick wins',
'initiatives': []
},
{
'phase': 'Acceleration',
'months': '3-9',
'focus': 'Execute core strategies',
'initiatives': []
},
{
'phase': 'Scale',
'months': '9-18',
'focus': 'Expand and optimize',
'initiatives': []
},
{
'phase': 'Transform',
'months': '18+',
'focus': 'Long-term transformation',
'initiatives': []
}
]
# Assign initiatives to phases
for i, option in enumerate(options[:4]):
if i == 0:
phases[0]['initiatives'].append(option['name'])
elif i == 1:
phases[1]['initiatives'].append(option['name'])
elif i == 2:
phases[2]['initiatives'].append(option['name'])
else:
phases[3]['initiatives'].append(option['name'])
roadmap['phases'] = phases
# Define key milestones
roadmap['milestones'] = [
{'month': 3, 'milestone': 'Complete foundation phase', 'success_criteria': 'Core team hired, processes defined'},
{'month': 6, 'milestone': 'First major initiative launch', 'success_criteria': 'KPIs showing positive trend'},
{'month': 12, 'milestone': 'Strategic review', 'success_criteria': 'ROI demonstrated, strategy validated'},
{'month': 18, 'milestone': 'Scale achievement', 'success_criteria': 'Market position improved, financial targets met'}
]
# Resource requirements
roadmap['resource_requirements'] = {
'leadership': 'C-suite alignment and commitment',
'financial': '$X million investment over 18 months',
'human': 'Additional 20-30 FTEs across functions',
'technology': 'Platform upgrades and new tools',
'external': 'Consultants and advisors as needed'
}
# Success metrics
roadmap['success_metrics'] = [
'Revenue growth: 25% YoY',
'Market share: +5 percentage points',
'EBITDA margin: +8 percentage points',
'Customer NPS: >70',
'Employee engagement: >80%'
]
return roadmap
def _generate_recommendations(self, results: Dict) -> List[str]:
"""Generate strategic recommendations"""
recommendations = []
# Based on overall score
score = results['strategic_health_score']
if score < 40:
recommendations.append('🚨 URGENT: Immediate turnaround required - consider bringing in crisis management team')
recommendations.append('Focus on cash preservation and core business stabilization')
elif score < 60:
recommendations.append('⚠️ Strategic repositioning needed - prioritize 2-3 key initiatives')
recommendations.append('Strengthen weak pillars before pursuing growth')
elif score < 80:
recommendations.append('✓ Solid position - focus on selective improvements and growth')
recommendations.append('Invest in innovation and market expansion')
else:
recommendations.append('⭐ Excellent position - maintain momentum and explore bold moves')
recommendations.append('Consider industry disruption or category creation')
# Based on specific weaknesses
for pillar, analysis in results['pillar_analysis'].items():
if analysis['score'] < 50:
if pillar == 'market_position':
recommendations.append(f'Strengthen {pillar}: Launch competitive differentiation program')
elif pillar == 'financial_health':
recommendations.append(f'Improve {pillar}: Implement profitability improvement plan')
elif pillar == 'organizational_capability':
recommendations.append(f'Build {pillar}: Invest in talent and culture transformation')
# Based on opportunities
if results['framework_analysis']['porter_analysis']['overall_attractiveness'] > 70:
recommendations.append('Industry is attractive - consider aggressive expansion')
# Risk-based recommendations
if results['risk_assessment']['overall_risk'] > 60:
recommendations.append('High risk profile - implement comprehensive risk management')
return recommendations
def analyze_strategy(company_data: Dict) -> str:
"""Main function to analyze strategy"""
analyzer = StrategyAnalyzer()
results = analyzer.analyze_strategic_position(company_data)
# Format output
output = [
f"=== Strategic Analysis Report ===",
f"Company: {results['company']}",
f"Date: {results['timestamp'][:10]}",
f"",
f"STRATEGIC HEALTH SCORE: {results['strategic_health_score']}/100",
f"",
"Strategic Pillars:"
]
for pillar, analysis in results['pillar_analysis'].items():
output.append(f" {pillar.replace('_', ' ').title()}: {analysis['score']:.1f} ({analysis['level']})")
for factor in analysis['factors'][:2]: # Show top 2 factors
output.append(f" • {factor['factor']}: {factor['status']}")
output.extend([
f"",
"Strategic Options:"
])
for i, option in enumerate(results['strategic_options'][:3], 1):
output.append(f"\n{i}. {option['name']} (Priority: {option['priority']}/10)")
output.append(f" Type: {option['type']}")
output.append(f" Investment: {option['investment']}")
output.append(f" Timeframe: {option['timeframe']}")
output.append(f" Impact: {option['expected_impact']}")
output.extend([
f"",
f"Risk Assessment:",
f" Overall Risk: {results['risk_assessment']['overall_risk']:.1f}%",
f" Execution Risk: {results['risk_assessment']['execution_risk']:.1f}%",
f" Market Risk: {results['risk_assessment']['market_risk']:.1f}%",
f" Financial Risk: {results['risk_assessment']['financial_risk']:.1f}%",
f"",
"Strategic Roadmap:"
])
for phase in results['roadmap']['phases'][:3]:
output.append(f" {phase['phase']} ({phase['months']}): {phase['focus']}")
for initiative in phase['initiatives']:
output.append(f" • {initiative}")
output.extend([
f"",
"Key Recommendations:"
])
for rec in results['recommendations'][:5]:
output.append(f" • {rec}")
return '\n'.join(output)
if __name__ == "__main__":
# Example usage
example_company = {
'name': 'TechCorp Inc.',
'market_position': {
'market_share': 35,
'brand_strength': 65,
'competitive_advantage': 70,
'customer_loyalty': 60
},
'financial_health': {
'revenue_growth': 45,
'profitability': 40,
'cash_flow': 55,
'unit_economics': 60
},
'organizational_capability': {
'talent': 70,
'culture': 65,
'leadership': 75,
'agility': 60
},
'growth_potential': {
'market_size': 80,
'expansion_opportunities': 70,
'product_pipeline': 60,
'partnerships': 55
},
'competitive_forces': {
'rivalry': 70,
'suppliers': 40,
'buyers': 60,
'substitutes': 50,
'new_entrants': 45
},
'context': {
'industry_disruption': True,
'cash_available': 150000000
},
'timeline': 18
}
print(analyze_strategy(example_company))
FILE:cfo-advisor/SKILL.md
---
name: "cfo-advisor"
description: "Financial leadership for startups and scaling companies. Financial modeling, unit economics, fundraising strategy, cash management, and board financial packages. Use when building financial models, analyzing unit economics, planning fundraising, managing cash runway, preparing board materials, or when user mentions CFO, burn rate, runway, fundraising, unit economics, LTV, CAC, term sheets, or financial strategy."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: cfo-leadership
updated: 2026-03-05
python-tools: burn_rate_calculator.py, unit_economics_analyzer.py, fundraising_model.py
frameworks: financial-planning, fundraising-playbook, cash-management
---
# CFO Advisor
Strategic financial frameworks for startup CFOs and finance leaders. Numbers-driven, decisions-focused.
This is **not** a financial analyst skill. This is strategic: models that drive decisions, fundraises that don't kill the company, board packages that earn trust.
## Keywords
CFO, chief financial officer, burn rate, runway, unit economics, LTV, CAC, fundraising, Series A, Series B, term sheet, cap table, dilution, financial model, cash flow, board financials, FP&A, SaaS metrics, ARR, MRR, net dollar retention, gross margin, scenario planning, cash management, treasury, working capital, burn multiple, rule of 40
## Quick Start
```bash
# Burn rate & runway scenarios (base/bull/bear)
python scripts/burn_rate_calculator.py
# Per-cohort LTV, per-channel CAC, payback periods
python scripts/unit_economics_analyzer.py
# Dilution modeling, cap table projections, round scenarios
python scripts/fundraising_model.py
```
## Key Questions (ask these first)
- **What's your burn multiple?** (Net burn ÷ Net new ARR. > 2x is a problem.)
- **If fundraising takes 6 months instead of 3, do you survive?** (If not, you're already behind.)
- **Show me unit economics per cohort, not blended.** (Blended hides deterioration.)
- **What's your NDR?** (> 100% means you grow without signing a single new customer.)
- **What are your decision triggers?** (At what runway do you start cutting? Define now, not in a crisis.)
## Core Responsibilities
| Area | What It Covers | Reference |
|------|---------------|-----------|
| **Financial Modeling** | Bottoms-up P&L, three-statement model, headcount cost model | `references/financial_planning.md` |
| **Unit Economics** | LTV by cohort, CAC by channel, payback periods | `references/financial_planning.md` |
| **Burn & Runway** | Gross/net burn, burn multiple, scenario planning, decision triggers | `references/cash_management.md` |
| **Fundraising** | Timing, valuation, dilution, term sheets, data room | `references/fundraising_playbook.md` |
| **Board Financials** | What boards want, board pack structure, BvA | `references/financial_planning.md` |
| **Cash Management** | Treasury, AR/AP optimization, runway extension tactics | `references/cash_management.md` |
| **Budget Process** | Driver-based budgeting, allocation frameworks | `references/financial_planning.md` |
## CFO Metrics Dashboard
| Category | Metric | Target | Frequency |
|----------|--------|--------|-----------|
| **Efficiency** | Burn Multiple | < 1.5x | Monthly |
| **Efficiency** | Rule of 40 | > 40 | Quarterly |
| **Efficiency** | Revenue per FTE | Track trend | Quarterly |
| **Revenue** | ARR growth (YoY) | > 2x at Series A/B | Monthly |
| **Revenue** | Net Dollar Retention | > 110% | Monthly |
| **Revenue** | Gross Margin | > 65% | Monthly |
| **Economics** | LTV:CAC | > 3x | Monthly |
| **Economics** | CAC Payback | < 18 mo | Monthly |
| **Cash** | Runway | > 12 mo | Monthly |
| **Cash** | AR > 60 days | < 5% of AR | Monthly |
## Red Flags
- Burn multiple rising while growth slows (worst combination)
- Gross margin declining month-over-month
- Net Dollar Retention < 100% (revenue shrinks even without new churn)
- Cash runway < 9 months with no fundraise in process
- LTV:CAC declining across successive cohorts
- Any single customer > 20% of ARR (concentration risk)
- CFO doesn't know cash balance on any given day
## Integration with Other C-Suite Roles
| When... | CFO works with... | To... |
|---------|-------------------|-------|
| Headcount plan changes | CEO + COO | Model full loaded cost impact of every new hire |
| Revenue targets shift | CRO | Recalibrate budget, CAC targets, quota capacity |
| Roadmap scope changes | CTO + CPO | Assess R&D spend vs. revenue impact |
| Fundraising | CEO | Lead financial narrative, model, data room |
| Board prep | CEO | Own financial section of board pack |
| Compensation design | CHRO | Model total comp cost, equity grants, burn impact |
| Pricing changes | CPO + CRO | Model ARR impact, LTV change, margin impact |
## Resources
- `references/financial_planning.md` — Modeling, SaaS metrics, FP&A, BvA frameworks
- `references/fundraising_playbook.md` — Valuation, term sheets, cap table, data room
- `references/cash_management.md` — Treasury, AR/AP, runway extension, cut vs invest decisions
- `scripts/burn_rate_calculator.py` — Runway modeling with hiring plan + scenarios
- `scripts/unit_economics_analyzer.py` — Per-cohort LTV, per-channel CAC
- `scripts/fundraising_model.py` — Dilution, cap table, multi-round projections
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- Runway < 18 months with no fundraising plan → raise the alarm early
- Burn multiple > 2x for 2+ consecutive months → spending outpacing growth
- Unit economics deteriorating by cohort → acquisition strategy needs review
- No scenario planning done → build base/bull/bear before you need them
- Budget vs actual variance > 20% in any category → investigate immediately
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "How much runway do we have?" | Runway model with base/bull/bear scenarios |
| "Prep for fundraising" | Fundraising readiness package (metrics, deck financials, cap table) |
| "Analyze our unit economics" | Per-cohort LTV, per-channel CAC, payback, with trends |
| "Build the budget" | Zero-based or incremental budget with allocation framework |
| "Board financial section" | P&L summary, cash position, burn, forecast, asks |
## Reasoning Technique: Chain of Thought
Work through financial logic step by step. Show all math. Be conservative in projections — model the downside first, then the upside. Never round in your favor.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:cfo-advisor/references/cash_management.md
# Cash Management Reference
Cash is the oxygen of a startup. You can be unprofitable for years. You cannot be out of cash for a day.
---
## 1. Cash Flow Management
### The Cash Equation
```
Ending Cash = Beginning Cash
+ Cash collected from customers
- Cash paid to employees
- Cash paid to vendors
- Cash paid for infrastructure
- Debt service
+/- Financing activities
Note: This is NOT the P&L. Revenue recognition ≠ cash collected.
```
### Where Cash Hides (and Leaks)
**Cash sources you might be under-using:**
- Deferred revenue (annual billing locks in cash 12 months early)
- Customer deposits on enterprise contracts
- Vendor payment terms (Net 60 instead of Net 30 = free float)
- AWS/GCP startup credits (often $25K–$100K available, widely unused)
- Revenue-based financing on predictable MRR
- Venture debt (non-dilutive, available post-Series A)
**Cash drains that sneak up on you:**
- Annual software licenses paid in Q1 (budget for the lump sum)
- Event sponsorships (often 6-12 months in advance)
- Recruiting fees (15-25% of first-year salary, due on hire)
- Legal fees (data room prep, fundraise close = $50K–$200K surprise)
- Late-paying enterprise customers (Net 60 in contract, pays Net 90 in practice)
### Cash Flow vs P&L: The Gap
**Scenario: $1M enterprise deal signed December 31**
```
P&L impact (accrual):
December revenue: $83K (1/12 of annual)
Cash impact:
If billed annually upfront: +$1,000K in December (GREAT)
If billed quarterly: +$250K in December (good)
If billed monthly: +$83K in December (fine)
If Net 60 terms: +$0 in December, +$83K in February (cash drag)
```
**The CFO's job:** Maximize the timing difference between cash in and cash out.
- Collect from customers as early as possible (annual upfront, early payment discounts)
- Pay vendors as late as possible (maximize payment terms)
- Never confuse deferred revenue (a liability) with actual cash (it is cash — just count it right)
---
## 2. Treasury and Banking Strategy
### Account Structure
```
Operating Account (primary bank):
Balance: 3-6 months of operating expenses
Purpose: Payroll, vendor payments, day-to-day ops
Product: Business checking or high-yield business savings
Bank: Chase, SVB successor (First Citizens), Mercury, Brex
Reserve Account (secondary or same bank):
Balance: Everything above operating float
Purpose: Reserve; move to operating as needed
Product: Money market fund or T-Bill ladder
Target yield (2024-2025): 4.5%–5.2%
Products: Vanguard VMFXX, Fidelity SPAXX, or direct T-Bills via TreasuryDirect
Emergency Account (separate bank):
Balance: 1-2 months expenses
Purpose: If primary bank has issues (SVB taught this lesson)
Product: Business savings
```
**FDIC coverage:** $250K per depositor per institution. For balances above $250K at a single bank, either:
- Use CDARS/ICS (bank sweeps into multiple FDIC-insured accounts automatically)
- Spread across multiple banks
- Move excess to T-Bills (backed by US government, not FDIC, but safer)
**After SVB (March 2023):** Every CFO should have at least 2 banking relationships. If one bank fails or freezes, you can make payroll.
### Yield on Cash
At $3M cash, the difference between 0% (checking) and 5% (T-Bills) is $150K/year.
That's a month of runway for a $150K/month burn company. **Get yield on reserves.**
```
Monthly yield on $3M at 5%: ~$12,500
Annual: ~$150,000
This is not optional. Set it up once and automate.
```
---
## 3. AR/AP Optimization
### Accounts Receivable: Get Paid Faster
**Billing model impact on cash:**
```
Annual Upfront Quarterly Monthly Net 30 Monthly
Cash Day 1: 100% of ACV 25% of ACV 8.3% 0%
Cash Month 2: 0% (done) 0% 8.3% 8.3%
12-month total: 100% 100% 100% 100%
For $100K ACV customer, Year 1 cash:
Annual upfront: $100K immediately
Monthly Net 30: $8.3K × 11 months = $91.7K (1 month lag)
Cash benefit: $100K vs $91.7K = $8.3K benefit + no collection risk
```
**Push for annual billing. Make it easy with a discount:**
```
"Pay annually and get 2 months free (16% discount)"
Most SMB customers will take this.
Enterprise: use MSA structure with annual invoicing, not month-to-month.
```
**AR Aging Policy:**
```
> 0-30 days: Current. No action.
> 30-60 days: Friendly reminder from AR team.
> 60-90 days: Escalate to Customer Success.
> 90 days: CFO or CEO-level outreach. Consider collections.
> 120 days: Reserve for bad debt. Legal/collections.
Reserve policy: 50% of 90-120 day AR, 100% of > 120 days
```
**What slows down collections:**
- Wrong contact (billing contact vs. user) — get finance contact during onboarding
- Enterprise PO required — know this upfront, not when invoice is due
- Credit holds or budget freeze — your CSM should surface these early
- Invoice errors — every wrong invoice extends payment by 30-60 days
### Accounts Payable: Pay Slower
**Standard terms by vendor type:**
```
SaaS tools: Net 30 default. Push for Net 45 or Net 60 at scale.
Cloud providers: Pay as you go. Apply for credits first.
Professional services (agencies, lawyers): Net 30 minimum. Get Net 45 where possible.
Rent/office: Whatever the lease says. Negotiate quarterly payments if you can.
Payroll: Pay on time. Never delay payroll. Ever.
```
**Early payment discount trap:**
```
"2/10 Net 30" means: 2% discount if you pay in 10 days, else pay in 30.
Annual cost of NOT taking this: 2% × (365/(30-10)) = ~36% APY
ALWAYS take early payment discounts > 2%.
Never take discounts < 1%.
```
**AP workflow:**
1. All invoices → finance inbox (not individual employees)
2. Approval required above threshold ($500 for startups)
3. Pay at end of terms, not when invoice arrives
4. Batch payments weekly (not daily) to reduce processing overhead
---
## 4. Runway Extension Tactics
Use these when you need to extend runway without raising. Ranked by speed and impact.
### Tier 1: Fast Cash (Days)
**Annual billing campaign:**
```
Target: Existing monthly customers
Offer: 2 months free (16% discount) or 1 month free (8% discount) for annual upfront
Process: CSM-led email campaign to all monthly customers
Impact: $X MRR × 12 × conversion rate = immediate cash injection
Timeline: 2-4 weeks
No dilution. No debt. High impact.
```
**Prepayment incentive for pipeline:**
```
For deals in late stage, offer annual upfront pricing with 10-15% discount.
Close rate may increase. Cash timing dramatically improves.
```
### Tier 2: Cost Control (2-4 Weeks)
**Hiring freeze:**
```
Every unfilled role = salary × 1.25 per month.
For a 30-person company, 3 open roles at $150K average:
Monthly savings: 3 × $150K × 1.25 / 12 = $47K/month
Over 6 months: $280K
Impact: Immediate. No blood.
```
**Software audit:**
```
Pull all credit card charges and ACH debits.
Cancel any subscription not used in 30 days.
Typical savings: $3K-$15K/month at Series A stage.
Tools: Vendr, Spendesk, or just a spreadsheet of recurring charges.
```
**Cloud cost optimization:**
```
Right-size instances (dev/staging don't need prod-scale)
Reserve instances (1-year reserved = 30-40% savings vs on-demand)
Delete unused resources (load balancers, IPs, old snapshots)
Typical savings: 20-35% of current cloud bill
```
### Tier 3: Vendor Renegotiation (2-6 Weeks)
**Payment term extension:**
```
Ask key vendors for Net 60 instead of Net 30.
$500K in AP × 30 days = $500K × (30/365) = ~$41K cash float improvement
Won't always work, but vendors often say yes to good customers.
```
**Renewal timing:**
```
Push annual renewals to later in the year.
Preserve cash for Q1 (typically heaviest sales hiring quarter).
```
**Vendor credits:**
```
AWS: AWS Activate (up to $100K for qualified startups)
GCP: Google for Startups (up to $200K)
Azure: Microsoft for Startups (up to $150K)
Stripe: Revenue share programs
Hubspot: Startup pricing (90% off)
```
### Tier 4: Financing (Weeks to Months)
**Revenue-based financing:**
```
Providers: Clearco, Capchase, Pipe, Arc
Structure: Advance 3-6 months of MRR. Repay with % of monthly revenue.
Cost: Typically 6-12% annualized.
Speed: 1-2 weeks to close.
When to use: Bridge to next ARR milestone before raising equity.
When NOT to use: When burn rate is structural (will consume the advance fast).
```
**Venture debt:**
```
Providers: SVB (now First Citizens), Western Technology Investment, Hercules, TriplePoint
Structure: Term loan, typically 3-6x monthly gross burn
Interest: Prime + 2-4% + warrants
When available: Post-Series A, when revenue is predictable
Typical timing: Add alongside an equity round (don't raise debt when you need equity)
Impact: Extends runway 3-6 months without dilution
When NOT to use: If you might trip financial covenants (minimum cash, revenue)
```
**Convertible bridge:**
```
Existing investors write bridge note: $500K-$2M at favorable terms.
Structure: Converts at discount (10-20%) or cap into next equity round.
When to use: You're 60-90 days from closing an equity round and need cash to get there.
When NOT to use: As a long-term strategy. Bridge-to-bridge is a death spiral.
```
### Tier 5: Structural Cost Reduction (Weeks + Impact on Morale)
**Salary deferrals (founders first):**
```
Founders take 20-30% salary reduction, accrued for future repayment.
Signals commitment to team and investors.
Only ask employees to follow if founders go first.
Always pay market rate to key non-founder employees — you can't afford to lose them.
```
**Reduction in force (RIF):**
```
Threshold: If burn multiple > 3x and growth < 20% YoY, a RIF is likely necessary.
Sizing: Model to achieve at least 12 months runway without fundraising.
Rule: Don't do a RIF twice. Size it right the first time.
Two small RIFs destroy morale worse than one decisive one.
Process: Legal counsel required. WARN Act (60-day notice) if > 100 employees.
Focus cuts: G&A and underperforming sales roles first. Protect engineering and key revenue.
```
---
## 5. When to Cut vs When to Invest
### The Framework
**Cut when:**
- Burn multiple > 2x and growth is decelerating
- Runway < 9 months with no fundraise imminent
- LTV:CAC declining for 3+ consecutive months
- Any spend category with no measurable return in 90 days
- Headcount in functions not directly tied to near-term revenue or product-market fit
**Invest when:**
- Magic number > 1 (every dollar in S&M returns > $1 in gross profit)
- LTV:CAC > 3x in a specific channel (pour money in)
- Gross margin > 70% (unit economics are healthy; growth is the constraint)
- Cohort data improving (retention getting better → LTV going up → invest in growth)
- CAC payback < 12 months (you get your money back fast enough to keep reinvesting)
### The False Economy Trap
**Don't cut:**
- Top-of-funnel demand gen that generates qualified pipeline (if CAC payback is < 12 months, this is your best investment)
- Engineering capacity on core product (technical debt compounds and slows you down permanently)
- Key account managers on your largest customers (churn from top customers is catastrophic)
**Cut these first:**
- Conference sponsorships with no measurable pipeline
- Tools and subscriptions with < 5 users or < 30% utilization
- Agency spend that could be done in-house
- Roadmap items that aren't tied to retention or expansion revenue
- Any G&A spend that isn't legally required
### Decision Triggers (Pre-Define These)
Don't make these decisions in a crisis. Define the triggers now:
```
At 12 months runway: Review all discretionary spend. Start fundraise process.
At 9 months runway: Implement hiring freeze. Fundraise is mandatory.
At 6 months runway: Cut non-essential spend 20%. If no fundraise term sheet, run RIF model.
At 4 months runway: Execute RIF. Explore all financing options. Notify board.
At 3 months runway: Emergency plan only. All options on table (bridge, strategic, wind down).
```
---
## Key Formulas
```python
# Net burn
net_burn = gross_burn - revenue_collected
# Runway (months)
runway_months = cash_balance / net_burn
# Cash conversion cycle
ccc = days_sales_outstanding + days_inventory_held - days_payable_outstanding
# Lower CCC = better cash efficiency
# Days Sales Outstanding (DSO)
dso = (accounts_receivable / revenue) * 30 # monthly revenue
# Days Payable Outstanding (DPO)
dpo = (accounts_payable / cogs) * 30 # target: maximize this
# Working capital
working_capital = current_assets - current_liabilities
# Quick ratio (liquidity)
quick_ratio_liquidity = (cash + ar) / current_liabilities
# Target: > 1.5 (you can pay short-term obligations without selling assets)
# Free cash flow
fcf = operating_cash_flow - capex
```
FILE:cfo-advisor/references/financial_planning.md
# Financial Planning Reference
Startup financial modeling frameworks. Build models that drive decisions, not models that impress investors.
---
## 1. Startup Financial Modeling
### Bottoms-Up vs Top-Down
**Top-down model (don't use for operating):**
```
TAM = $10B
SOM = 1% = $100M
Revenue = $100M in year 5
```
This is marketing. You cannot manage a company against these numbers.
**Bottoms-up model (use this):**
```
Year 1 Revenue Build:
Sales headcount: 3 AEs by Q1, +2 in Q2, +3 in Q4
Ramp curve: Month 1-3 = 25%, Month 4-6 = 75%, Month 7+ = 100%
Quota per ramped AE: $600K ARR
Effective quota (weighted for ramp): $1.2M ARR in Year 1
Win rate: 25%
Average deal: $48K ACV
Pipeline needed: $1.2M / 25% = $4.8M ARR pipeline
Required meetings to create that pipeline: $4.8M / (conversion 20%) / ($48K ACV × 0.5 to meeting) = ~200 meetings
```
Now you have something actionable. You know how many SDR calls, how many marketing leads, what conversion rate you need to hold. Every assumption is visible and challengeable.
### Building the Operating Model
#### Revenue Engine
**New ARR Model (SaaS):**
```
Month N New ARR:
= Quota-carrying reps (fully ramped equivalent)
× Attainment rate (typically 70-80% of quota)
× Average deal size
+ PLG / self-serve (if applicable)
Quota-carrying reps (ramped equivalent):
= Sum(each rep × their ramp factor)
Ramp schedule:
Month 1-2: 0% (onboarding)
Month 3: 25%
Month 4-6: 50%
Month 7-9: 75%
Month 10+: 100%
```
**ARR Bridge (most important recurring visual):**
```
Beginning ARR
+ New ARR (new logos)
+ Expansion ARR (upsells, seat growth)
- Churned ARR (cancellations)
- Contraction ARR (downgrades)
= Ending ARR
Net ARR Added = New + Expansion - Churn - Contraction
Net Dollar Retention (NDR):
= (Beginning ARR + Expansion - Churn - Contraction) / Beginning ARR × 100
Target: > 110% for growth-stage SaaS
World-class: > 130% (Snowflake, Twilio-tier)
```
**MRR and ARR Relationship:**
```
ARR = MRR × 12 (simple, always use this)
Never mix monthly and annual contracts in MRR without normalization.
Annual contract booked = ACV / 12 = monthly contribution to ARR
Multi-year contracts: book each year at annual value (not multi-year total)
```
#### Headcount Model
Headcount is usually 60-80% of total costs. Model it carefully.
```
For each role:
- Start date
- Department
- Annual salary (from salary bands)
- Loaded cost (salary × 1.25-1.45 depending on benefits + recruiting method)
- Productive from (ramp period)
- Impact on revenue (for revenue-generating roles)
Total headcount cost = Σ (each FTE × loaded cost × months active / 12)
```
**Department headcount ratios (Series A benchmarks):**
```
Sales (S&M): 20-30% of headcount
Engineering/Product (R&D): 40-50% of headcount
Customer Success: 15-20% of headcount
G&A: 10-15% of headcount
```
#### COGS Model
Gross margin is the most important long-term indicator of business quality.
**COGS for SaaS:**
```
1. Hosting / Infrastructure (AWS, GCP, Azure)
- Scale with customer count or usage
- Should be 5-15% of ARR for mature SaaS
- If > 20%: infrastructure optimization needed
2. Customer Success headcount
- Ratio: 1 CSM per $1M-$3M ARR (varies by segment)
- SMB: 1 CSM per $500K ARR (high-touch required)
- Enterprise: 1 CSM per $2-5M ARR (strategic accounts)
3. Third-party licensing / APIs
- Per-customer or usage-based pass-through costs
- Critical to model at scale (margin killer if not tracked)
4. Payment processing
- 2.2-2.9% of revenue for Stripe/Braintree
- Can negotiate to 1.8-2.2% at scale (> $5M ARR)
```
**Gross Margin targets:**
```
SaaS: > 65% acceptable, > 75% good, > 80% exceptional
Marketplace: 50-70%
Hardware + software: 40-60%
Services + software: 30-50%
```
**If gross margin < 65%:**
- Infrastructure cost optimization (rightsizing, reserved instances)
- CS headcount review (automation, pooled CSMs)
- Pricing model review (usage-based pricing if cost is usage-driven)
- Third-party cost renegotiation
#### Opex Model
```
Sales & Marketing:
- AE/SDR/SE salaries + OTE (on-target earnings)
- Marketing programs (demand gen budget)
- Tools and technology (CRM, SEO, ads platforms)
- Events and travel
- Benchmark: 40-60% of revenue at growth stage, targeting < 30% at scale
Research & Development:
- Engineering salaries
- Product management
- Design
- Technical infrastructure for development
- Benchmark: 20-35% of revenue
General & Administrative:
- Finance, legal, HR, admin
- Office costs
- SaaS tools / software licenses
- D&O insurance
- Benchmark: 8-15% (target < 10% at scale)
```
### Financial Model Do's and Don'ts
| Do | Don't |
|----|-------|
| Build assumptions tab with all inputs | Hardcode numbers in formulas |
| Model monthly (not quarterly) at early stage | Use annual model for first 3 years |
| Start with headcount plan, build costs from it | Guess at expense line items |
| Show model to actual customers or users | Show model to investors before internal stress-test |
| Version your model | Overwrite old versions |
| Reconcile cash flow to P&L monthly | Trust P&L without cash flow model |
| Include a sensitivity table | Present single-scenario forecast |
---
## 2. Three-Statement Model for Startups
### Why All Three Matter
The P&L tells you if you're profitable. The cash flow statement tells you if you're alive. The balance sheet tells you if you're solvent.
Startups that only track P&L miss the gap between revenue recognition and cash collection.
### P&L Structure
```
Q1 Q2 Q3 Q4 FY
Revenue
Subscription ARR $400K $520K $680K $840K $2,440K
Professional Svcs $40K $50K $60K $65K $215K
Total Revenue $440K $570K $740K $905K $2,655K
COGS
Infrastructure $35K $42K $52K $62K $191K
CS Headcount $75K $75K $100K $100K $350K
3rd Party Licensing $15K $18K $22K $28K $83K
Total COGS $125K $135K $174K $190K $624K
Gross Profit $315K $435K $566K $715K $2,031K
Gross Margin 71.6% 76.3% 76.5% 79.0% 76.5%
Operating Expenses
Sales & Marketing $380K $420K $480K $520K $1,800K
Research & Dev $320K $340K $380K $400K $1,440K
General & Admin $120K $130K $140K $150K $540K
Total Opex $820K $890K $1000K $1070K $3,780K
EBITDA ($505K) ($455K) ($434K) ($355K) ($1,749K)
EBITDA Margin (114.8%)(79.8%) (58.6%) (39.2%) (65.9%)
```
### Cash Flow Statement
```
Q1 Q2 Q3 Q4
Operating Activities
Net Income ($510K) ($460K) ($440K) ($360K)
Add: D&A $8K $8K $8K $10K
Working Capital Changes:
AR increase ($45K) ($50K) ($60K) ($55K)
AP increase $20K $15K $20K $15K
Deferred Rev change $80K $60K $80K $90K
Operating Cash Flow ($447K) ($427K) ($392K) ($300K)
Investing Activities
Capex ($15K) ($8K) ($10K) ($12K)
Free Cash Flow ($462K) ($435K) ($402K) ($312K)
Financing Activities
None $0 $0 $0 $0
Net Change in Cash ($462K) ($435K) ($402K) ($312K)
Beginning Cash $3,500K $3,038K $2,603K $2,201K
Ending Cash $3,038K $2,603K $2,201K $1,889K
Runway (months) 13.1 12.1 10.9 10.1
```
**Key insight from this model:**
The deferred revenue offset (customers paying annually upfront) is reducing cash burn by ~$80-90K/quarter versus a pure monthly billing model. This is the CFO's lever — push for annual billing.
### Balance Sheet: The Startup Version
At early stage, track these specifically:
```
Assets:
Cash: Your lifeline. Monitor daily.
Accounts Receivable: What customers owe you. Age it monthly.
Prepaid Expenses: Software licenses, insurance paid upfront.
Liabilities:
Accounts Payable: What you owe vendors. Maximize terms.
Accrued Liabilities: Salaries owed, commissions earned but not paid.
Deferred Revenue: Customer prepayments. Liability until service delivered, but cash is yours.
Debt/Convertible Notes: Face value + interest accrual.
Equity:
Common Stock: Founder shares
Preferred Stock: Investor shares
APIC: Additional paid-in capital
Accumulated Deficit: Your running losses (expected for startups)
```
---
## 3. SaaS Metrics That Matter
### The Hierarchy of SaaS Metrics
```
Tier 1 (existential): ARR, Runway, Net Dollar Retention
Tier 2 (strategic): Gross Margin, Burn Multiple, LTV:CAC
Tier 3 (operational): CAC Payback, Churn Rate, ACV
Tier 4 (diagnostic): Logo Churn vs Revenue Churn, Expansion Rate, NPS
```
Never report Tier 4 metrics to your board if Tier 1 metrics are off-track.
### Core Metric Definitions
**ARR (Annual Recurring Revenue):**
```
ARR = Sum of all active annual contract values (normalized to annual)
What it is NOT: bookings, billings, or TCV
When to use MRR: Companies with mostly monthly contracts
When to use ARR: Companies with majority annual contracts
```
**Net Dollar Retention (NDR / NRR):**
```
NDR = (Beginning MRR + Expansion MRR - Churned MRR - Contraction MRR)
/ Beginning MRR × 100
The benchmark everyone quotes: 100% means existing customers are flat.
> 100% means existing customers grow revenue on their own.
World-class (Snowflake, Datadog): 130%+
Why it matters: NDR > 100% means revenue growth even if you sign zero new customers.
At NDR = 120% and $5M ARR: you will reach $7M ARR in 24 months without a single new sale.
```
**Gross Revenue Retention (GRR):**
```
GRR = (Beginning MRR - Churned MRR - Contraction MRR) / Beginning MRR × 100
GRR measures the floor of your retention (ignoring expansion).
GRR is always ≤ NDR.
Target: > 85% for SMB SaaS, > 90% for mid-market, > 95% for enterprise.
```
**Logo Churn vs Revenue Churn:**
```
Logo churn: % of customers who cancel (ignores size)
Revenue churn: % of ARR that cancels (accounts for size)
Why the distinction matters:
You could have 10% logo churn but 3% revenue churn (churning small customers)
Or 5% logo churn but 12% revenue churn (churning large customers) — much worse
Report both. If they diverge significantly, investigate immediately.
```
**ACV (Annual Contract Value):**
```
ACV = Total contract value / contract term in years
Not to be confused with ARR (which only counts recurring, not one-time fees)
Rising ACV: You're moving upmarket (good for efficiency, check if ICP is changing)
Falling ACV: You're moving downmarket (check burn multiple — may not be economic)
```
**Rule of 40:**
```
Rule of 40 = Revenue Growth Rate % + EBITDA Margin %
Target: > 40%
Example: 60% growth + (-15%) EBITDA margin = 45. Passing.
Example: 20% growth + 5% EBITDA margin = 25. Failing at growth stage.
At early stage (< $5M ARR): Rule of 40 doesn't apply. Growth is the only metric.
At growth stage ($5-20M ARR): Starting to matter.
At scale ($20M+ ARR): Board and investors will hold you to this.
```
---
## 4. FP&A for Startups: What to Measure When
### Metrics by Stage
**Pre-seed / Seed (< $1M ARR):**
```
Focus on: Cash, pipeline, customer conversations
Measure: Monthly cash burn, weeks of runway, NPS / customer satisfaction
Don't obsess over: EBITDA margin, gross margin (too early)
Frequency: Weekly cash check, monthly everything else
```
**Series A ($1-5M ARR):**
```
Focus on: Repeatable sales, unit economics
Measure: MRR growth, LTV:CAC, CAC payback by channel, gross margin
Don't obsess over: Profitability, G&A efficiency
Build now: Monthly financial close (< 5 business days), basic FP&A model
Frequency: Monthly board pack, weekly leadership metrics
```
**Series B ($5-20M ARR):**
```
Focus on: Scalable go-to-market, operational efficiency
Measure: NDR, burn multiple, revenue per FTE, OKR attainment
Start building: Budget vs actuals, department-level P&L
Build now: Finance team (first financial controller), ERP or NetSuite
Frequency: Monthly board pack + quarterly deep dive
```
**Series C+ ($20M+ ARR):**
```
Focus on: Path to profitability, market leadership
Measure: Rule of 40, free cash flow, CAC efficiency by segment
Must have: FP&A team, full three-statement model, 5-year plan
Frequency: Monthly financial close (< 3 business days), quarterly earnings prep
```
### Reporting Cadence
**Weekly (CFO + leadership):**
- Cash balance (CFO checks daily, reports weekly)
- Pipeline / sales metrics (if in a sales-led motion)
- Any metric that changed dramatically vs. prior week
**Monthly (board + leadership):**
- Full financial dashboard (ARR, gross margin, burn, runway)
- Budget vs actual with explanations for > 10% variances
- Unit economics update
- Headcount change summary
**Quarterly (board + investors):**
- Full three-statement model vs budget
- Cohort analysis update
- Scenario planning review and trigger assessment
- Next quarter outlook
---
## 5. Budget vs Actual Analysis Framework
### The Purpose of BvA
Budget vs actual is not about being right. It's about understanding *why* you were wrong, so you can make better decisions.
The CFO who reports "we missed budget by 15%" without explanation is failing. The CFO who says "we missed budget by 15% because enterprise deals took 30 more days to close than modeled — here's what we're doing about it" is doing their job.
### BvA Template
```
Category Budget Actual $ Var % Var Explanation
-------------------------------------------------------------------
ARR $2,400K $2,280K ($120K) (5%) 2 enterprise deals slipped to Q1
New ARR $400K $350K ($50K) (13%) Above
Expansion ARR $120K $140K $20K 17% PLG motion outperforming
Churn ($60K) ($80K) ($20K) (33%) 2 unexpected SMB churns (now fixed)
Gross Margin 75.0% 73.2% -1.8% n/a Infrastructure over-provisioned
S&M Spend $820K $840K ($20K) (2%) Within tolerance
R&D Spend $680K $710K ($30K) (4%) Backfill hire started month early
G&A Spend $140K $148K ($8K) (6%) Legal fees for new customer contract
Cash Burn (net) $580K $648K ($68K) (12%) Driven by ARR shortfall + costs
Runway (mo) 14.5 13.0 (1.5) n/a Tracking; fundraise target unchanged
```
### Variance Thresholds
```
< ±5%: Note in appendix, no explanation needed in main pack
5-10%: One-line explanation required
> 10%: Full paragraph: what happened, why, what changes
> 20%: Board conversation required (model assumption was wrong, or unexpected event)
```
### Forecasting vs Budgeting
**Budget:** Set at start of year. Fixed expectation. Updated quarterly.
**Forecast:** Rolling 3-month outlook. Updated monthly. Should converge with budget over time.
```
Common mistake: Treating forecast as wishful thinking ("what we hope happens")
Correct approach: Forecast is your best current estimate given all known information.
If forecast diverges from budget by > 15%, the budget is wrong.
Reforecast and communicate to board.
```
**Rolling forecast (recommended for startups):**
```
Always have a 12-month forward model.
Update it monthly with actuals replacing the first month.
The forecast should always reflect your current operational reality, not your hope.
```
---
## Key Formulas Reference
```python
# ARR and growth
ARR_growth_yoy = (ending_ARR - beginning_ARR) / beginning_ARR
# Net Dollar Retention
NDR = (beginning_MRR + expansion_MRR - churn_MRR - contraction_MRR) / beginning_MRR
# Burn Multiple
burn_multiple = net_cash_burn / net_new_ARR
# Rule of 40
rule_of_40 = revenue_growth_pct + ebitda_margin_pct
# LTV (SaaS)
LTV = (ARPA * gross_margin_pct) / monthly_churn_rate
# CAC Payback (months)
cac_payback = CAC / (ARPA * gross_margin_pct)
# Magic Number (sales efficiency)
magic_number = (net_new_ARR * 4) / prior_quarter_S_and_M_spend
# Gross margin
gross_margin = (revenue - COGS) / revenue
# Quick Ratio (growth efficiency)
quick_ratio = (new_MRR + expansion_MRR) / (churned_MRR + contraction_MRR)
# Target: > 4 for high-growth SaaS
```
FILE:cfo-advisor/references/fundraising_playbook.md
# Fundraising Playbook
From timing to close. What investors actually look for, how valuation works, and the term sheet clauses that matter.
---
## 1. When to Raise
**Optimal timing:**
```
Target: 18-24 months runway post-close
Minimum: 12 months runway post-close (leaves no buffer for slip)
Start process when: 9-12 months runway remaining
→ 3-6 months for process (typically 4-5 months for Series A/B)
→ Leaves 3-6 months buffer if process drags
Never start when: < 6 months runway
→ You're negotiating from desperation
→ Investors can smell it
→ Terms get worse, or you don't close at all
```
**Rule:** Your leverage is maximum when you don't *need* to raise. Raise from a position of momentum, not necessity.
---
## 2. What Investors Look For at Each Stage
### Pre-seed
- Team (are these people credible for this problem?)
- Problem clarity (is the problem real and meaningful?)
- Early signal (any customers paying, waitlist, prototype)
- Market size (worth building a VC-scale company?)
**Typical ask:** $500K–$2M | **Typical valuation:** $3M–$10M pre-money
### Seed
- Product-market signal (customers using and paying)
- Founding team with domain expertise
- ARR: $100K–$1M (or strong usage for PLG)
- Clear hypothesis for what Series A looks like
**Typical ask:** $2M–$5M | **Typical valuation:** $8M–$20M pre-money
### Series A
Investors are buying a *repeatable sales motion*. Not just customers — a machine.
**What they need to see:**
- ARR: $1M–$5M growing > 100% YoY
- LTV:CAC > 2.5x (and improving)
- Net Dollar Retention > 100%
- CAC Payback < 18 months
- Gross margin > 65%
- At least 5-10 reference customers (not just lighthouse)
- Sales motion that converts without the founder closing every deal
**Typical ask:** $8M–$15M | **Typical valuation:** $25M–$60M pre-money
### Series B
Investors are buying *scalable go-to-market*. Can you pour fuel on the fire?
**What they need to see:**
- ARR: $5M–$20M growing > 100% YoY
- LTV:CAC > 3x, CAC Payback < 18 months
- Sales capacity model (hiring plan → pipeline → revenue)
- NDR > 110% (expansion motion working)
- Some proof of market expansion (new segments, geographies, use cases)
- Path to category leadership
**Typical ask:** $15M–$40M | **Typical valuation:** $60M–$200M pre-money
### Series C and Beyond
Investors are buying *market leadership* and *path to profitability*.
**What they need to see:**
- ARR: $20M+ (often $30-50M for credible Series C)
- Rule of 40 > 40 (or credible path)
- Gross margin > 70%
- NDR > 115%
- Evidence of market leadership (brand, win rates, analyst mentions)
- Clear path to $100M+ ARR
---
## 3. Valuation Methods
### Revenue Multiples (Primary Method for SaaS)
```
Pre-money Valuation = ARR × Revenue Multiple
Revenue multiple benchmarks (2024-2025):
> 100% YoY growth: 8x–15x ARR
50-100% YoY growth: 4x–8x ARR
20-50% YoY growth: 2x–4x ARR
< 20% YoY growth: 1x–2x ARR
Adjustments:
NDR > 120%: +1x–2x premium
Gross margin > 75%: +0.5x–1x premium
Burn multiple < 1x: +0.5x–1x premium
Capital efficient: Investors pay up for efficiency
Declining growth: Compress multiple aggressively
```
### The Investor's Math (Know This)
Every VC has a required return. Work backwards from their constraints:
```
Investor targets: 3x fund return
Fund size: $200M, check size: $15M (initial), $25M (with follow-on)
Ownership at exit needed: 15%
At 15% ownership: needs $25M / 15% = $167M post-money valuation
Exit needed to return 3x on that check: $25M × 10 = $250M company value
(10x because most deals fail, winners must carry the fund)
Implication: If you think you'll exit for $150M, that VC will pass or price you accordingly.
```
This is why Series A investors rarely lead rounds where they can't see a $300M+ exit path. It's not about your business being bad — it's about fund math.
### Comparable Company Analysis
For later stages (Series B+):
```
1. Find 5-10 comparable public SaaS companies
2. Calculate their EV/NTM Revenue multiples (use latest data)
3. Apply a private market discount (typically 20-40% vs public comps)
4. Adjust for your growth rate relative to comps
Example (2024):
Public SaaS comps: 6x NTM Revenue (median)
Private discount: 30%
Adjusted: ~4.2x
Your NTM Revenue: $8M
Implied valuation: ~$33M pre-money
```
### DCF (Late Stage Only)
DCF is unreliable for early-stage startups (terminal value dominates, growth rate assumptions are fantasy). Use it as a sanity check at Series C+, not as the primary valuation method.
---
## 4. Term Sheet Breakdown
### Liquidation Preference (Most Important Economic Term)
This determines who gets paid first in an exit — and how much.
```
1x Non-Participating Preferred (BEST for founders):
Investor gets 1x money back OR converts to common (their choice).
At acquisition: investor takes larger of {1x invested} or {% ownership × proceeds}
Example: $10M invested, exits at $100M, owns 20%
Option A: $10M (1x)
Option B: $20M (20% of $100M)
Investor takes $20M. Founders split $80M.
1x Participating Preferred (WORSE for founders):
Investor gets 1x money back AND participates in remaining proceeds.
Example: same scenario
$10M (1x) + 20% of remaining $90M = $10M + $18M = $28M
Founders split $72M instead of $80M
Cost to founders: $8M (10% of exit value)
2x Participating (RED FLAG):
Investor gets 2x back AND participates.
Only accept under duress. Push hard against this.
Full Ratchet Anti-Dilution (AVOID):
Down-round triggers full repricing of investor shares to new (lower) price.
Founders get massively diluted. Never accept if alternatives exist.
```
### Anti-Dilution Protection
```
Broad-based weighted average (standard):
Adjusts investor conversion price based on all dilutive securities.
Most founder-friendly anti-dilution. Accept this.
Narrow-based weighted average (slightly worse):
Same mechanism but uses smaller denominator.
Gives investors slightly more protection. Usually acceptable.
Full ratchet (avoid):
Price drops to whatever the new round prices at.
Devastating in down rounds. Fight this.
```
### Pro-Rata Rights
```
Standard pro-rata: Investor can maintain their % ownership in future rounds.
Reasonable. Accept for major investors.
Super pro-rata: Investor can increase their % in future rounds.
Caps your ability to bring in new lead investors.
Avoid unless the investor is exceptional and you want them in future rounds.
Major investor threshold: Typically investors with > $500K–$1M check get pro-rata.
Don't give pro-rata to every small check — clogs future rounds.
```
### Board Composition
```
Seed (3 members): 2 founders, 1 lead investor
Series A (5 members): 2 founders, 2 investors, 1 independent
Series B (5-7 seats): Watch for investor majority — negotiate hard
Rule: Founders should retain majority through Series A.
Independent director should be your choice, not investor's.
Never accept investor majority before Series C.
Board observer rights: Common for smaller investors. No vote but present in meetings.
Limit to 1-2 observers or meetings become unwieldy.
```
### Other Terms That Matter
```
Drag-along: Majority can force minority shareholders to vote for acquisition.
Standard and reasonable. Check what threshold triggers drag.
Information rights: Investors get financial statements.
Standard. Monthly for major investors, quarterly for others.
Redemption rights: Investors can force buyback after X years.
Push to remove or add carve-outs for insufficient funds.
No-shop clause: You can't shop the term sheet to other investors.
Standard (14-30 days). Reasonable.
Exclusivity: Stronger version of no-shop. Sometimes includes no other fundraise discussions.
Acceptable for 30 days; push back on > 45 days.
```
---
## 5. Cap Table Management
### Dilution Planning Model
Run this before every round. Know your number before walking into any negotiation.
```
Pre-Seed Post-Seed Post-A Post-B Post-C
Founder A 45.0% 36.0% 26.5% 21.2% 18.7%
Founder B 45.0% 36.0% 26.5% 21.2% 18.7%
Angel 1 5.0% 4.0% 2.9% 2.4% 2.1%
Angel 2 5.0% 4.0% 2.9% 2.4% 2.1%
Seed Fund - 12.0% 8.8% 7.1% 6.2%
Option Pool - 8.0% 12.0% 10.0% 8.0%
Series A - - 20.4% 16.3% 14.4%
Series B - - - 19.5% 17.2%
Series C - - - - 12.6%
Round size / pre-money:
Pre-Seed: $500K / $9M pre = 5% dilution
Seed: $2M / $8M pre = 20% dilution (includes 8% pool)
Series A: $10M / $38M pre = 20.8% dilution (pool refresh to 12%)
Series B: $20M / $80M pre = 20% dilution
Series C: $30M / $170M pre = 15% dilution
```
**Option pool shuffle:** Investors often require you to create/expand the option pool *before* the round closes, which dilutes existing shareholders (not the incoming investor). Model this explicitly — a 20% round with a 5% pool expansion is really 24%+ dilution to founders.
### Cap Table Hygiene
```
Tools: Carta, Pulley, Capshare (all acceptable)
Never: Track cap table in a spreadsheet past seed stage. Errors compound.
Keep it clean:
- Repurchase departed co-founder shares immediately (don't let unvested shares linger)
- Convert SAFEs to equity cleanly at each priced round
- Document every grant with a board resolution
- Cliff + vesting for ALL employees and founders (standard: 1-year cliff, 4-year vest)
- 409A valuation required before every option grant (IRS requirement)
```
---
## 6. Data Room Preparation
### Core Documents (Required)
```
Financial:
□ 3 years historical financials (or all history if < 3 years)
□ Monthly P&L and cash flow (last 24 months)
□ Current financial model (18-24 months forward)
□ Budget vs actual (last 4 quarters)
□ Cap table (fully diluted, with all SAFEs/convertibles modeled)
□ Bank statements (last 3-6 months)
Legal:
□ Certificate of incorporation + all amendments
□ All prior financing documents (SAFEs, convertible notes, stock purchase agreements)
□ Cap table (Carta/Pulley export)
□ IP assignment agreements (all founders and employees)
□ Material contracts (top 10 customers, key vendors)
□ Employee list (titles, start dates, salaries, equity grants)
Product & Business:
□ Product demo / walkthrough video
□ Architecture overview (for technical investors)
□ Customer case studies (3-5 named references)
□ NPS / CSAT data
□ Competitive landscape analysis
Metrics:
□ MRR/ARR by month (all history)
□ Cohort retention chart
□ CAC by channel
□ LTV by cohort
□ NPS trend
```
### What Investors Actually Check First
In order of typical priority during due diligence:
1. **Cap table** — Is it clean? Any concerning structures?
2. **Cohort retention** — Is churn improving or deteriorating?
3. **Revenue quality** — What % is recurring? Any one-time or non-recurring?
4. **Top 10 customers** — Concentration risk? Any logos at risk?
5. **Bank statements** — Does cash match what was reported?
6. **IP assignments** — Does the company own its IP? (Founders who didn't assign IP kill deals)
### Red Flags That Kill Deals
- Missing IP assignment agreements for founders (most common deal killer at early stage)
- Cap table with > 20 angels/small investors (messy, hard to get consent for future rounds)
- Customer concentration > 30% in single customer without explanation
- Revenue recognition issues (booking ARR on contracts that allow easy cancellation)
- Cohort data that gets worse in later cohorts
- Bank balance doesn't match reported cash position
---
## 7. Investor Communication Cadence
### During Fundraise
```
Week 1-2: Warm intro sourcing, LP/network mapping
Week 3-6: First meetings (aim for 20-30 first meetings)
Week 7-10: Partner meetings, deep dives, due diligence
Week 11-14: Term sheets, negotiation
Week 15-18: Legal, closing
```
**Parallel process is essential.** Never negotiate with one investor at a time. Competition is your leverage.
### Post-Close: Investor Updates
Monthly investor update (send within 10 days of month-end):
```
Subject: [Company] Monthly Update — [Month Year]
Highlights (3 bullets max):
• [Biggest win]
• [Biggest learning/challenge]
• [What we're focused on next month]
Metrics:
ARR: $X (+X% MoM)
Net new ARR: $X
Gross margin: X%
Cash: $X (X months runway)
Headcount: X
Asks (be specific):
• Looking for intro to [persona/company] for [specific reason]
• Need advisor with experience in [specific area]
• [Other concrete ask]
```
**Why this matters:** Investors who are informed and engaged are better positioned to help when you need it. The investor who hasn't heard from you in 6 months is less likely to write a bridge check or make a warm intro when you ask.
---
## Key Formulas
```python
# Post-money valuation
post_money = pre_money + investment_amount
# Investor ownership %
ownership_pct = investment_amount / post_money
# Dilution to existing shareholders
dilution = investment_amount / post_money # as a fraction
# New shares issued
new_shares = (investment_amount / post_money) * total_post_shares
# equivalent: new_shares = pre_money_shares * (investment_amount / pre_money)
# Option pool expansion impact (pool shuffle)
# Creating X% option pool pre-close dilutes founders:
pool_shares_needed = target_pct * (pre_shares + new_round_shares + pool_shares_needed)
# Solve: pool_shares_needed = target_pct * (pre_shares + new_round_shares) / (1 - target_pct)
# LTV:CAC ratio
ltv_cac = ltv / cac # target: > 3x
# CAC payback (months)
payback_months = cac / (arpa * gross_margin_pct)
```
FILE:cfo-advisor/scripts/burn_rate_calculator.py
#!/usr/bin/env python3
"""
Burn Rate & Runway Calculator
==============================
Models startup runway across base/bull/bear scenarios, incorporating
a hiring plan and revenue trajectory. Outputs months of runway,
cash-out dates, and decision trigger points.
Usage:
python burn_rate_calculator.py
python burn_rate_calculator.py --csv # export to CSV
Stdlib only. No dependencies.
"""
import argparse
import csv
import io
import sys
from dataclasses import dataclass, field
from datetime import date, timedelta
from typing import Optional
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
@dataclass
class HiringEntry:
"""A planned hire."""
month: int # months from model start (1-indexed)
role: str
department: str # "sales", "engineering", "cs", "ga"
annual_salary: float
benefits_pct: float = 0.22 # benefits as % of salary
recruiting_cost: float = 0.0 # one-time recruiting fee
@dataclass
class RevenueEntry:
"""Monthly revenue data point (historical or projected)."""
month: int
mrr: float # monthly recurring revenue
one_time: float = 0.0
@dataclass
class ModelConfig:
"""Master configuration for a runway scenario."""
name: str
starting_cash: float
starting_mrr: float
starting_headcount: int
avg_loaded_salary: float # average fully-loaded salary per current employee
base_non_headcount_opex: float # monthly non-headcount costs (infra, tools, etc.)
gross_margin_pct: float # 0.0–1.0
mrr_growth_rate: float # monthly MoM growth rate, 0.0–1.0
hiring_plan: list[HiringEntry] = field(default_factory=list)
model_months: int = 24
start_date: Optional[date] = None
@dataclass
class MonthResult:
"""Single month output."""
month: int
label: str # e.g. "Month 1 (Apr 2025)"
mrr: float
gross_profit: float
headcount: int
headcount_cost: float # total loaded headcount cost this month
other_opex: float
gross_burn: float
net_burn: float
cash_start: float
cash_end: float
runway_months: float # projected runway from this month
cumulative_new_arr: float # for burn multiple
# ---------------------------------------------------------------------------
# Core calculator
# ---------------------------------------------------------------------------
class RunwayCalculator:
def __init__(self, config: ModelConfig):
self.cfg = config
def run(self) -> list[MonthResult]:
cfg = self.cfg
results = []
# Build headcount schedule: month -> list of new hires starting that month
hire_by_month: dict[int, list[HiringEntry]] = {}
for h in cfg.hiring_plan:
hire_by_month.setdefault(h.month, []).append(h)
# Track existing employees
active_employees: list[dict] = []
for _ in range(cfg.starting_headcount):
active_employees.append({
"monthly_loaded": cfg.avg_loaded_salary / 12 * 1.0,
"start_month": 0,
})
cash = cfg.starting_cash
mrr = cfg.starting_mrr
cumulative_new_arr = 0.0
starting_mrr = cfg.starting_mrr
for m in range(1, cfg.model_months + 1):
# Process new hires this month
one_time_recruiting = 0.0
if m in hire_by_month:
for hire in hire_by_month[m]:
monthly_loaded = (
hire.annual_salary * (1 + hire.benefits_pct) / 12
)
active_employees.append({
"monthly_loaded": monthly_loaded,
"start_month": m,
})
one_time_recruiting += hire.recruiting_cost
# Revenue this month
mrr = mrr * (1 + cfg.mrr_growth_rate)
gross_profit = mrr * cfg.gross_margin_pct
# Headcount cost
headcount_cost = sum(e["monthly_loaded"] for e in active_employees)
headcount_cost += one_time_recruiting
# Other opex (infra, SaaS tools, office, etc.)
other_opex = cfg.base_non_headcount_opex
# Burn
gross_burn = headcount_cost + other_opex
net_burn = gross_burn - gross_profit
# Cash
cash_start = cash
cash = cash - net_burn
cash_end = cash
# Projected runway from this month (using current net burn rate)
runway = cash_end / net_burn if net_burn > 0 else float("inf")
# Cumulative new ARR (for burn multiple calc)
new_mrr_added = mrr - starting_mrr if m == 1 else mrr - results[-1].mrr
cumulative_new_arr += new_mrr_added * 12
# Label
if cfg.start_date:
month_date = date(
cfg.start_date.year,
cfg.start_date.month,
1,
) + timedelta(days=32 * (m - 1))
month_date = month_date.replace(day=1)
label = f"Month {m:02d} ({month_date.strftime('%b %Y')})"
else:
label = f"Month {m:02d}"
results.append(MonthResult(
month=m,
label=label,
mrr=mrr,
gross_profit=gross_profit,
headcount=len(active_employees),
headcount_cost=headcount_cost,
other_opex=other_opex,
gross_burn=gross_burn,
net_burn=net_burn,
cash_start=cash_start,
cash_end=cash_end,
runway_months=runway,
cumulative_new_arr=cumulative_new_arr,
))
# Stop if cash runs out
if cash_end <= 0:
break
return results
def cash_out_date(self, results: list[MonthResult]) -> Optional[str]:
"""Return the label of the month cash runs out, or None if model survives."""
for r in results:
if r.cash_end <= 0:
return r.label
return None
def burn_multiple(self, results: list[MonthResult]) -> float:
"""Burn multiple = total net burn / total net new ARR over model period."""
total_net_burn = sum(r.net_burn for r in results if r.net_burn > 0)
first_mrr = results[0].mrr / (1 + self.cfg.mrr_growth_rate) # starting mrr
total_new_arr = (results[-1].mrr - first_mrr) * 12
if total_new_arr <= 0:
return float("inf")
return total_net_burn / total_new_arr
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt_k(value: float) -> str:
"""Format as $Xk or $X.XM."""
if abs(value) >= 1_000_000:
return f".2fM"
if abs(value) >= 1_000:
return f".0fK"
return f".0f"
def print_summary(name: str, results: list[MonthResult], calc: RunwayCalculator) -> None:
cash_out = calc.cash_out_date(results)
bm = calc.burn_multiple(results)
last = results[-1]
first = results[0]
print(f"\n{'='*60}")
print(f" SCENARIO: {name}")
print(f"{'='*60}")
print(f" Months modeled: {len(results)}")
print(f" Cash out: {cash_out or 'Does not run out in model period'}")
print(f" Ending cash: {fmt_k(last.cash_end)}")
print(f" Final runway: {last.runway_months:.1f} months")
print(f" Starting MRR: {fmt_k(first.mrr)}")
print(f" Ending MRR: {fmt_k(last.mrr)}")
print(f" Ending headcount: {last.headcount}")
print(f" Burn multiple: {bm:.2f}x")
print(f" Avg net burn: {fmt_k(sum(r.net_burn for r in results)/len(results))}/mo")
# Decision triggers
print(f"\n Decision Triggers:")
triggers = {9: "⚠️ START FUNDRAISE", 6: "🔴 COST REDUCTION PLAN", 4: "🚨 EXECUTE CUTS / BRIDGE"}
shown = set()
for r in results:
for threshold, label in triggers.items():
if r.runway_months <= threshold and threshold not in shown:
print(f" {r.label}: {label} (runway = {r.runway_months:.1f} mo)")
shown.add(threshold)
def print_monthly_table(results: list[MonthResult], max_rows: int = 24) -> None:
header = f"{'Month':<22} {'MRR':>10} {'Hdct':>6} {'Net Burn':>12} {'Cash':>12} {'Runway':>8}"
print(f"\n{header}")
print("-" * len(header))
for r in results[:max_rows]:
runway_str = f"{r.runway_months:.1f}mo" if r.runway_months != float("inf") else "∞"
print(
f"{r.label:<22} "
f"{fmt_k(r.mrr):>10} "
f"{r.headcount:>6} "
f"{fmt_k(r.net_burn):>12} "
f"{fmt_k(r.cash_end):>12} "
f"{runway_str:>8}"
)
def export_csv(scenarios: list[tuple[str, list[MonthResult]]]) -> str:
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow([
"Scenario", "Month", "Label", "MRR", "Gross Profit", "Headcount",
"Headcount Cost", "Other Opex", "Gross Burn", "Net Burn",
"Cash Start", "Cash End", "Runway Months"
])
for name, results in scenarios:
for r in results:
writer.writerow([
name, r.month, r.label,
round(r.mrr, 2), round(r.gross_profit, 2), r.headcount,
round(r.headcount_cost, 2), round(r.other_opex, 2),
round(r.gross_burn, 2), round(r.net_burn, 2),
round(r.cash_start, 2), round(r.cash_end, 2),
round(r.runway_months, 2),
])
return buf.getvalue()
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
def make_sample_configs() -> list[ModelConfig]:
"""
Sample company: Series A SaaS startup
- $3M cash on hand (post Series A)
- $125K MRR (~$1.5M ARR)
- 18 employees, $150K avg salary
- $80K/mo non-headcount opex (infra, tools, office)
- 72% gross margin
"""
common_kwargs = dict(
starting_cash=3_000_000,
starting_mrr=125_000,
starting_headcount=18,
avg_loaded_salary=150_000,
base_non_headcount_opex=80_000,
gross_margin_pct=0.72,
model_months=24,
start_date=date(2025, 1, 1),
)
# Base: 10% MoM growth, moderate hiring
base_hiring = [
HiringEntry(month=2, role="AE #1", department="sales", annual_salary=120_000, recruiting_cost=18_000),
HiringEntry(month=3, role="Senior SWE #1", department="engineering", annual_salary=160_000, recruiting_cost=24_000),
HiringEntry(month=5, role="SDR #1", department="sales", annual_salary=80_000, recruiting_cost=12_000),
HiringEntry(month=6, role="CSM #1", department="cs", annual_salary=90_000, recruiting_cost=13_500),
HiringEntry(month=8, role="AE #2", department="sales", annual_salary=120_000, recruiting_cost=18_000),
HiringEntry(month=9, role="Senior SWE #2", department="engineering", annual_salary=165_000, recruiting_cost=24_750),
HiringEntry(month=12, role="Controller", department="ga", annual_salary=130_000, recruiting_cost=19_500),
HiringEntry(month=14, role="AE #3", department="sales", annual_salary=125_000, recruiting_cost=18_750),
HiringEntry(month=15, role="ML Engineer", department="engineering", annual_salary=175_000, recruiting_cost=26_250),
HiringEntry(month=18, role="AE #4", department="sales", annual_salary=125_000, recruiting_cost=18_750),
]
# Bull: 15% MoM growth, full hiring plan
bull_hiring = base_hiring + [
HiringEntry(month=4, role="Marketing Manager", department="sales", annual_salary=110_000, recruiting_cost=16_500),
HiringEntry(month=7, role="Senior SWE #3", department="engineering", annual_salary=165_000, recruiting_cost=24_750),
HiringEntry(month=10, role="AE #5", department="sales", annual_salary=125_000, recruiting_cost=18_750),
HiringEntry(month=13, role="DevOps Engineer", department="engineering", annual_salary=150_000, recruiting_cost=22_500),
HiringEntry(month=16, role="AE #6", department="sales", annual_salary=125_000, recruiting_cost=18_750),
]
# Bear: 5% MoM growth, hiring freeze after month 3
bear_hiring = [
HiringEntry(month=2, role="AE #1", department="sales", annual_salary=120_000, recruiting_cost=18_000),
HiringEntry(month=3, role="Senior SWE #1", department="engineering", annual_salary=160_000, recruiting_cost=24_000),
]
return [
ModelConfig(name="BULL (15% MoM, full hiring)", mrr_growth_rate=0.15, hiring_plan=bull_hiring, **common_kwargs),
ModelConfig(name="BASE (10% MoM, planned hiring)", mrr_growth_rate=0.10, hiring_plan=base_hiring, **common_kwargs),
ModelConfig(name="BEAR ( 5% MoM, hiring freeze M3+)", mrr_growth_rate=0.05, hiring_plan=bear_hiring, **common_kwargs),
ModelConfig(name="DISTRESS (0% growth, freeze now)", mrr_growth_rate=0.00, hiring_plan=[], **common_kwargs),
]
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description="Startup Burn Rate & Runway Calculator")
parser.add_argument("--csv", action="store_true", help="Export full monthly data as CSV to stdout")
parser.add_argument("--scenario", choices=["bull", "base", "bear", "distress", "all"], default="all")
args = parser.parse_args()
configs = make_sample_configs()
if args.scenario != "all":
configs = [c for c in configs if args.scenario.upper() in c.name.upper()]
all_results: list[tuple[str, list[MonthResult]]] = []
print("\n" + "="*60)
print(" BURN RATE & RUNWAY CALCULATOR")
print(" Sample Company: Series A SaaS Startup")
print(" Starting cash: $3M | Starting MRR: $125K | 18 employees")
print("="*60)
for cfg in configs:
calc = RunwayCalculator(cfg)
results = calc.run()
all_results.append((cfg.name, results))
print_summary(cfg.name, results, calc)
print_monthly_table(results)
# Comparison summary
print("\n" + "="*60)
print(" SCENARIO COMPARISON")
print("="*60)
print(f" {'Scenario':<40} {'Runway':>8} {'Cash Out':<30} {'Burn Mult':>10}")
print(" " + "-"*88)
for cfg, (name, results) in zip(configs, all_results):
calc = RunwayCalculator(cfg)
cash_out = calc.cash_out_date(results) or "Survives model period"
bm = calc.burn_multiple(results)
final_runway = results[-1].runway_months
runway_str = f"{final_runway:.1f}mo" if final_runway != float("inf") else "∞"
bm_str = f"{bm:.2f}x" if bm != float("inf") else "∞"
print(f" {name:<40} {runway_str:>8} {cash_out:<30} {bm_str:>10}")
print("\n Decision Trigger Reference:")
print(" 9 months runway → Start fundraise process")
print(" 6 months runway → Begin cost reduction planning")
print(" 4 months runway → Execute cuts; explore bridge financing")
print(" 3 months runway → Emergency plan only")
if args.csv:
print("\n\n--- CSV EXPORT ---\n")
sys.stdout.write(export_csv(all_results))
if __name__ == "__main__":
main()
FILE:cfo-advisor/scripts/fundraising_model.py
#!/usr/bin/env python3
"""
Fundraising Model
==================
Cap table management, dilution modeling, and multi-round scenario planning.
Know exactly what you're giving up before you walk into any negotiation.
Covers:
- Cap table state at each round
- Dilution per shareholder per round
- Option pool shuffle impact
- Multi-round projections (Seed → A → B → C)
- Return scenarios at different exit valuations
Usage:
python fundraising_model.py
python fundraising_model.py --exit 150 # model at $150M exit
python fundraising_model.py --csv
Stdlib only. No dependencies.
"""
import argparse
import csv
import io
import sys
from dataclasses import dataclass, field
from typing import Optional
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
@dataclass
class Shareholder:
"""A shareholder in the cap table."""
name: str
share_class: str # "common", "preferred", "option"
shares: float
invested: float = 0.0 # total cash invested
is_option_pool: bool = False
@dataclass
class RoundConfig:
"""Configuration for a financing round."""
name: str # e.g. "Series A"
pre_money_valuation: float
investment_amount: float
new_option_pool_pct: float = 0.0 # % of POST-money to allocate to new options
option_pool_pre_round: bool = True # True = pool created before round (dilutes founders)
lead_investor_name: str = "New Investor"
share_price_override: Optional[float] = None # if None, computed from valuation
@dataclass
class CapTableEntry:
"""A row in the cap table at a point in time."""
name: str
share_class: str
shares: float
pct_ownership: float
invested: float
is_option_pool: bool = False
@dataclass
class RoundResult:
"""Snapshot of cap table after a round closes."""
round_name: str
pre_money_valuation: float
investment_amount: float
post_money_valuation: float
price_per_share: float
new_shares_issued: float
option_pool_shares_created: float
total_shares: float
cap_table: list[CapTableEntry]
@dataclass
class ExitAnalysis:
"""Proceeds to each shareholder at an exit."""
exit_valuation: float
shareholder: str
shares: float
ownership_pct: float
proceeds_common: float # if all preferred converts to common
invested: float
moic: float # multiple on invested capital (for investors)
# ---------------------------------------------------------------------------
# Core cap table engine
# ---------------------------------------------------------------------------
class CapTable:
"""Manages a cap table through multiple rounds."""
def __init__(self):
self.shareholders: list[Shareholder] = []
self._total_shares: float = 0.0
def add_shareholder(self, sh: Shareholder) -> None:
self.shareholders.append(sh)
self._total_shares += sh.shares
def total_shares(self) -> float:
return sum(s.shares for s in self.shareholders)
def snapshot(self, label: str = "") -> list[CapTableEntry]:
total = self.total_shares()
return [
CapTableEntry(
name=s.name,
share_class=s.share_class,
shares=s.shares,
pct_ownership=s.shares / total if total > 0 else 0,
invested=s.invested,
is_option_pool=s.is_option_pool,
)
for s in self.shareholders
]
def execute_round(self, config: RoundConfig) -> RoundResult:
"""
Execute a financing round:
1. (Optional) Create option pool pre-round (dilutes existing shareholders)
2. Issue new shares to investor at round price
Returns a RoundResult with full cap table snapshot.
"""
current_total = self.total_shares()
# Step 1: Option pool shuffle (if pre-round)
option_pool_shares_created = 0.0
if config.new_option_pool_pct > 0 and config.option_pool_pre_round:
# Target: post-round option pool = new_option_pool_pct of total post-money shares
# Solve: pool_shares / (current_total + pool_shares + new_investor_shares) = target_pct
# This requires iteration because new_investor_shares also depends on pool_shares
# Simplification: create pool based on post-round total (slightly approximated)
target_post_round_pct = config.new_option_pool_pct
post_money = config.pre_money_valuation + config.investment_amount
# Estimate shares per dollar (price per share)
price_per_share = config.pre_money_valuation / current_total
new_investor_shares_estimate = config.investment_amount / price_per_share
# Pool shares needed so that pool / total_post = target_pct
total_post_estimate = current_total + new_investor_shares_estimate
pool_shares_needed = (target_post_round_pct * total_post_estimate) / (1 - target_post_round_pct)
# Check if existing pool is sufficient
existing_pool = next(
(s.shares for s in self.shareholders if s.is_option_pool), 0
)
additional_pool_needed = max(0, pool_shares_needed - existing_pool)
if additional_pool_needed > 0:
option_pool_shares_created = additional_pool_needed
# Add to existing pool or create new
pool_sh = next((s for s in self.shareholders if s.is_option_pool), None)
if pool_sh:
pool_sh.shares += additional_pool_needed
else:
self.shareholders.append(Shareholder(
name="Option Pool",
share_class="option",
shares=additional_pool_needed,
is_option_pool=True,
))
# Step 2: Price per share (after pool creation)
current_total_post_pool = self.total_shares()
if config.share_price_override:
price_per_share = config.share_price_override
else:
price_per_share = config.pre_money_valuation / current_total_post_pool
# Step 3: New shares for investor
new_shares = config.investment_amount / price_per_share
# Step 4: Add investor to cap table
self.shareholders.append(Shareholder(
name=config.lead_investor_name,
share_class="preferred",
shares=new_shares,
invested=config.investment_amount,
))
post_money = config.pre_money_valuation + config.investment_amount
total_post = self.total_shares()
return RoundResult(
round_name=config.name,
pre_money_valuation=config.pre_money_valuation,
investment_amount=config.investment_amount,
post_money_valuation=post_money,
price_per_share=price_per_share,
new_shares_issued=new_shares,
option_pool_shares_created=option_pool_shares_created,
total_shares=total_post,
cap_table=self.snapshot(),
)
def analyze_exit(self, exit_valuation: float) -> list[ExitAnalysis]:
"""
Simple exit analysis: all preferred converts to common, proceeds split pro-rata.
(Does not model liquidation preferences — see fundraising_playbook.md for that.)
"""
total = self.total_shares()
price_per_share = exit_valuation / total
results = []
for s in self.shareholders:
if s.is_option_pool:
continue # unissued options don't receive proceeds
proceeds = s.shares * price_per_share
moic = proceeds / s.invested if s.invested > 0 else 0.0
results.append(ExitAnalysis(
exit_valuation=exit_valuation,
shareholder=s.name,
shares=s.shares,
ownership_pct=s.shares / total,
proceeds_common=proceeds,
invested=s.invested,
moic=moic,
))
return sorted(results, key=lambda x: x.proceeds_common, reverse=True)
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt(value: float, prefix: str = "$") -> str:
if value == float("inf"):
return "∞"
if abs(value) >= 1_000_000:
return f"{prefix}{value/1_000_000:.2f}M"
if abs(value) >= 1_000:
return f"{prefix}{value/1_000:.0f}K"
return f"{prefix}{value:.2f}"
def print_round_result(result: RoundResult, prev_cap_table: Optional[list[CapTableEntry]] = None) -> None:
print(f"\n{'='*70}")
print(f" {result.round_name.upper()}")
print(f"{'='*70}")
print(f" Pre-money valuation: {fmt(result.pre_money_valuation)}")
print(f" Investment: {fmt(result.investment_amount)}")
print(f" Post-money valuation: {fmt(result.post_money_valuation)}")
print(f" Price per share: {fmt(result.price_per_share, '$')}")
print(f" New shares issued: {result.new_shares_issued:,.0f}")
if result.option_pool_shares_created > 0:
print(f" Option pool created: {result.option_pool_shares_created:,.0f} shares")
print(f" ⚠️ Pool created pre-round: dilutes existing shareholders, not new investor")
print(f" Total shares post: {result.total_shares:,.0f}")
print(f"\n {'Shareholder':<22} {'Shares':>12} {'Ownership':>10} {'Invested':>10} {'Δ Ownership':>12}")
print(" " + "-"*68)
prev_map = {e.name: e.pct_ownership for e in prev_cap_table} if prev_cap_table else {}
for entry in result.cap_table:
delta = ""
if entry.name in prev_map:
change = (entry.pct_ownership - prev_map[entry.name]) * 100
delta = f"{change:+.1f}pp"
elif not entry.is_option_pool:
delta = "new"
invested_str = fmt(entry.invested) if entry.invested > 0 else "-"
print(
f" {entry.name:<22} {entry.shares:>12,.0f} "
f"{entry.pct_ownership*100:>9.2f}% {invested_str:>10} {delta:>12}"
)
def print_exit_analysis(results: list[ExitAnalysis], exit_valuation: float) -> None:
print(f"\n{'='*70}")
print(f" EXIT ANALYSIS @ {fmt(exit_valuation)} (all preferred converts to common)")
print(f"{'='*70}")
print(f"\n {'Shareholder':<22} {'Ownership':>10} {'Proceeds':>12} {'Invested':>10} {'MOIC':>8}")
print(" " + "-"*65)
for r in results:
moic_str = f"{r.moic:.1f}x" if r.moic > 0 else "n/a"
invested_str = fmt(r.invested) if r.invested > 0 else "-"
print(
f" {r.shareholder:<22} {r.ownership_pct*100:>9.2f}% "
f"{fmt(r.proceeds_common):>12} {invested_str:>10} {moic_str:>8}"
)
print(f"\n Note: Does not model liquidation preferences.")
print(f" Participating preferred reduces founder proceeds in most real exits.")
print(f" See references/fundraising_playbook.md for full liquidation waterfall.")
def print_dilution_summary(rounds: list[RoundResult]) -> None:
print(f"\n{'='*70}")
print(f" DILUTION SUMMARY — FOUNDER PERSPECTIVE")
print(f"{'='*70}")
# Find all founders (common shareholders who aren't investors or option pool)
founder_names = []
for entry in rounds[0].cap_table:
if entry.share_class == "common" and not entry.is_option_pool:
founder_names.append(entry.name)
if not founder_names:
print(" No common shareholders found in initial cap table.")
return
header = f" {'Round':<16}" + "".join(f" {n:<16}" for n in founder_names) + f" {'Total Inv':>12}"
print(header)
print(" " + "-" * (16 + 18 * len(founder_names) + 14))
for result in rounds:
cap_map = {e.name: e for e in result.cap_table}
total_invested = sum(e.invested for e in result.cap_table if not e.is_option_pool)
row = f" {result.round_name:<16}"
for name in founder_names:
pct = cap_map[name].pct_ownership * 100 if name in cap_map else 0
row += f" {pct:>6.2f}% "
row += f" {fmt(total_invested):>12}"
print(row)
def export_csv_rounds(rounds: list[RoundResult]) -> str:
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["Round", "Shareholder", "Share Class", "Shares", "Ownership Pct",
"Invested", "Pre Money", "Post Money", "Price Per Share"])
for r in rounds:
for entry in r.cap_table:
writer.writerow([
r.round_name, entry.name, entry.share_class,
round(entry.shares, 0), round(entry.pct_ownership * 100, 4),
round(entry.invested, 2), round(r.pre_money_valuation, 0),
round(r.post_money_valuation, 0), round(r.price_per_share, 4),
])
return buf.getvalue()
# ---------------------------------------------------------------------------
# Sample data: typical two-founder Series A/B/C startup
# ---------------------------------------------------------------------------
def build_sample_model() -> tuple[CapTable, list[RoundResult]]:
"""
Sample company:
- 2 founders, started with 10M shares each
- 1M shares for early advisor
- Raises Pre-seed → Seed → Series A → Series B → Series C
"""
cap = CapTable()
SHARES_PER_FOUNDER = 4_000_000
SHARES_ADVISOR = 200_000
# Founding state
cap.add_shareholder(Shareholder("Founder A (CEO)", "common", SHARES_PER_FOUNDER))
cap.add_shareholder(Shareholder("Founder B (CTO)", "common", SHARES_PER_FOUNDER))
cap.add_shareholder(Shareholder("Advisor", "common", SHARES_ADVISOR))
rounds: list[RoundResult] = []
prev_cap = cap.snapshot()
# Round 1: Pre-seed — $500K at $4.5M pre, 10% option pool created
r1 = cap.execute_round(RoundConfig(
name="Pre-seed",
pre_money_valuation=4_500_000,
investment_amount=500_000,
new_option_pool_pct=0.10,
option_pool_pre_round=True,
lead_investor_name="Angel Syndicate",
))
rounds.append(r1)
prev_r1 = r1.cap_table[:]
# Round 2: Seed — $2M at $9M pre, expand option pool to 12%
r2 = cap.execute_round(RoundConfig(
name="Seed",
pre_money_valuation=9_000_000,
investment_amount=2_000_000,
new_option_pool_pct=0.12,
option_pool_pre_round=True,
lead_investor_name="Seed Fund",
))
rounds.append(r2)
# Round 3: Series A — $12M at $38M pre, refresh option pool to 15%
r3 = cap.execute_round(RoundConfig(
name="Series A",
pre_money_valuation=38_000_000,
investment_amount=12_000_000,
new_option_pool_pct=0.15,
option_pool_pre_round=True,
lead_investor_name="Series A Fund",
))
rounds.append(r3)
# Round 4: Series B — $25M at $95M pre, refresh pool to 12%
r4 = cap.execute_round(RoundConfig(
name="Series B",
pre_money_valuation=95_000_000,
investment_amount=25_000_000,
new_option_pool_pct=0.12,
option_pool_pre_round=True,
lead_investor_name="Series B Fund",
))
rounds.append(r4)
# Round 5: Series C — $40M at $185M pre, refresh pool to 10%
r5 = cap.execute_round(RoundConfig(
name="Series C",
pre_money_valuation=185_000_000,
investment_amount=40_000_000,
new_option_pool_pct=0.10,
option_pool_pre_round=True,
lead_investor_name="Series C Fund",
))
rounds.append(r5)
return cap, rounds
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description="Fundraising Model — Cap Table & Dilution")
parser.add_argument("--exit", type=float, default=250.0,
help="Exit valuation in $M for return analysis (default: 250)")
parser.add_argument("--csv", action="store_true", help="Export round data as CSV to stdout")
args = parser.parse_args()
exit_valuation = args.exit * 1_000_000
print("\n" + "="*70)
print(" FUNDRAISING MODEL — CAP TABLE & DILUTION ANALYSIS")
print(" Sample Company: Two-founder SaaS startup")
print(" Pre-seed → Seed → Series A → Series B → Series C")
print("="*70)
cap, rounds = build_sample_model()
# Print each round
prev = None
for r in rounds:
print_round_result(r, prev)
prev = r.cap_table
# Dilution summary table
print_dilution_summary(rounds)
# Exit analysis at specified valuation
exit_results = cap.analyze_exit(exit_valuation)
print_exit_analysis(exit_results, exit_valuation)
# Also print at 2x and 5x for sensitivity
print("\n Exit Sensitivity — Founder A Proceeds:")
print(f" {'Exit Valuation':<20} {'Founder A %':>12} {'Founder A $':>14} {'MOIC':>8}")
print(" " + "-"*56)
for mult in [0.5, 1.0, 1.5, 2.0, 3.0, 5.0]:
val = rounds[-1].post_money_valuation * mult
ex = cap.analyze_exit(val)
founder_a = next((r for r in ex if r.shareholder == "Founder A (CEO)"), None)
if founder_a:
print(f" {fmt(val):<20} {founder_a.ownership_pct*100:>11.2f}% "
f"{fmt(founder_a.proceeds_common):>14} {'n/a':>8}")
print("\n Key Takeaways:")
final = rounds[-1].cap_table
total = sum(e.shares for e in final)
founder_a_final = next((e for e in final if e.name == "Founder A (CEO)"), None)
if founder_a_final:
print(f" Founder A final ownership: {founder_a_final.pct_ownership*100:.2f}%")
total_raised = sum(e.invested for e in final)
print(f" Total capital raised: {fmt(total_raised)}")
print(f" Total shares outstanding: {total:,.0f}")
print(f" Final post-money: {fmt(rounds[-1].post_money_valuation)}")
print("\n Run with --exit <$M> to model proceeds at different exit valuations.")
print(" Example: python fundraising_model.py --exit 500")
if args.csv:
print("\n\n--- CSV EXPORT ---\n")
sys.stdout.write(export_csv_rounds(rounds))
if __name__ == "__main__":
main()
FILE:cfo-advisor/scripts/unit_economics_analyzer.py
#!/usr/bin/env python3
"""
Unit Economics Analyzer
========================
Per-cohort LTV, per-channel CAC, payback periods, and LTV:CAC ratios.
Never blended averages — those hide what's actually happening.
Usage:
python unit_economics_analyzer.py
python unit_economics_analyzer.py --csv
Stdlib only. No dependencies.
"""
import argparse
import csv
import io
import sys
from dataclasses import dataclass, field
from typing import Optional
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
@dataclass
class CohortData:
"""
Revenue data for a group of customers acquired in the same period.
Revenue is tracked monthly: revenue[0] = month 1, revenue[1] = month 2, etc.
"""
label: str # e.g. "Q1 2024"
acquisition_period: str # human-readable label
customers_acquired: int
total_cac_spend: float # total S&M spend to acquire this cohort
monthly_revenue: list[float] # revenue per month from this cohort
gross_margin_pct: float = 0.70 # blended gross margin for this cohort
@dataclass
class ChannelData:
"""Acquisition cost and customer data for a single channel."""
channel: str
spend: float
customers_acquired: int
avg_arpa: float # average revenue per account (monthly)
gross_margin_pct: float = 0.70
avg_monthly_churn: float = 0.02 # monthly churn rate for customers from this channel
@dataclass
class UnitEconomicsResult:
"""Computed unit economics for a cohort or channel."""
label: str
customers: int
cac: float
arpa: float # average revenue per account per month
gross_margin_pct: float
monthly_churn: float
ltv: float
ltv_cac_ratio: float
payback_months: float
# Cohort-specific
m1_revenue: Optional[float] = None
m6_revenue: Optional[float] = None
m12_revenue: Optional[float] = None
m24_revenue: Optional[float] = None
m12_ltv: Optional[float] = None # realized LTV through month 12
retention_m6: Optional[float] = None # % of M1 revenue retained at M6
retention_m12: Optional[float] = None
# ---------------------------------------------------------------------------
# Calculators
# ---------------------------------------------------------------------------
def calc_ltv(arpa: float, gross_margin_pct: float, monthly_churn: float) -> float:
"""
LTV = (ARPA × Gross Margin) / Monthly Churn Rate
Assumes constant churn (simplified; cohort method is more accurate).
"""
if monthly_churn <= 0:
return float("inf")
return (arpa * gross_margin_pct) / monthly_churn
def calc_payback(cac: float, arpa: float, gross_margin_pct: float) -> float:
"""
CAC Payback (months) = CAC / (ARPA × Gross Margin)
"""
denominator = arpa * gross_margin_pct
if denominator <= 0:
return float("inf")
return cac / denominator
def analyze_cohort(cohort: CohortData) -> UnitEconomicsResult:
"""Compute full unit economics for a cohort."""
n = cohort.customers_acquired
if n == 0:
raise ValueError(f"Cohort {cohort.label}: customers_acquired cannot be 0")
cac = cohort.total_cac_spend / n
# ARPA from month 1 revenue
m1_rev = cohort.monthly_revenue[0] if cohort.monthly_revenue else 0
arpa = m1_rev / n if n > 0 else 0
# Observed monthly churn from cohort data
# Use revenue decline from M1 to M12 to estimate churn
months_available = len(cohort.monthly_revenue)
if months_available >= 12:
m12_rev = cohort.monthly_revenue[11]
# Revenue retention over 12 months: (M12/M1)^(1/11) per month on average
# Implied monthly retention rate
if m1_rev > 0 and m12_rev > 0:
monthly_retention = (m12_rev / m1_rev) ** (1 / 11)
monthly_churn = 1 - monthly_retention
else:
monthly_churn = 0.02 # default
elif months_available >= 6:
m6_rev = cohort.monthly_revenue[5]
if m1_rev > 0 and m6_rev > 0:
monthly_retention = (m6_rev / m1_rev) ** (1 / 5)
monthly_churn = 1 - monthly_retention
else:
monthly_churn = 0.02
else:
monthly_churn = 0.02 # default if < 6 months data
# Clamp to reasonable range
monthly_churn = max(0.001, min(monthly_churn, 0.30))
ltv = calc_ltv(arpa, cohort.gross_margin_pct, monthly_churn)
payback = calc_payback(cac, arpa, cohort.gross_margin_pct)
ltv_cac = ltv / cac if cac > 0 else float("inf")
# Snapshot revenues
def rev_at(month_idx: int) -> Optional[float]:
if months_available > month_idx:
return cohort.monthly_revenue[month_idx]
return None
m6 = rev_at(5)
m12 = rev_at(11)
m24 = rev_at(23)
# Realized LTV through observed months (actual gross profit)
m12_ltv = sum(cohort.monthly_revenue[:12]) * cohort.gross_margin_pct if months_available >= 12 else None
# Retention rates
ret_m6 = (m6 / m1_rev) if (m6 is not None and m1_rev > 0) else None
ret_m12 = (m12 / m1_rev) if (m12 is not None and m1_rev > 0) else None
return UnitEconomicsResult(
label=cohort.label,
customers=n,
cac=cac,
arpa=arpa,
gross_margin_pct=cohort.gross_margin_pct,
monthly_churn=monthly_churn,
ltv=ltv,
ltv_cac_ratio=ltv_cac,
payback_months=payback,
m1_revenue=m1_rev,
m6_revenue=m6,
m12_revenue=m12,
m24_revenue=m24,
m12_ltv=m12_ltv,
retention_m6=ret_m6,
retention_m12=ret_m12,
)
def analyze_channel(ch: ChannelData) -> UnitEconomicsResult:
"""Compute unit economics for an acquisition channel."""
if ch.customers_acquired == 0:
raise ValueError(f"Channel {ch.channel}: customers_acquired cannot be 0")
cac = ch.spend / ch.customers_acquired
ltv = calc_ltv(ch.avg_arpa, ch.gross_margin_pct, ch.avg_monthly_churn)
payback = calc_payback(cac, ch.avg_arpa, ch.gross_margin_pct)
ltv_cac = ltv / cac if cac > 0 else float("inf")
return UnitEconomicsResult(
label=ch.channel,
customers=ch.customers_acquired,
cac=cac,
arpa=ch.avg_arpa,
gross_margin_pct=ch.gross_margin_pct,
monthly_churn=ch.avg_monthly_churn,
ltv=ltv,
ltv_cac_ratio=ltv_cac,
payback_months=payback,
)
# ---------------------------------------------------------------------------
# Blended metrics (for comparison)
# ---------------------------------------------------------------------------
def blended_cac(channels: list[ChannelData]) -> float:
total_spend = sum(c.spend for c in channels)
total_customers = sum(c.customers_acquired for c in channels)
return total_spend / total_customers if total_customers > 0 else 0
def blended_ltv(channels: list[ChannelData]) -> float:
"""Weighted average LTV by customers acquired."""
total_customers = sum(c.customers_acquired for c in channels)
if total_customers == 0:
return 0
weighted = sum(
calc_ltv(c.avg_arpa, c.gross_margin_pct, c.avg_monthly_churn) * c.customers_acquired
for c in channels
)
return weighted / total_customers
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt(value: float, prefix: str = "$", decimals: int = 0) -> str:
if value == float("inf"):
return "∞"
if abs(value) >= 1_000_000:
return f"{prefix}{value/1_000_000:.2f}M"
if abs(value) >= 1_000:
return f"{prefix}{value/1_000:.1f}K"
return f"{prefix}{value:.{decimals}f}"
def pct(value: Optional[float]) -> str:
if value is None:
return "n/a"
return f"{value*100:.1f}%"
def rating(ltv_cac: float, payback: float) -> str:
if ltv_cac == float("inf"):
return "∞"
if ltv_cac >= 5 and payback <= 12:
return "🟢 Excellent"
if ltv_cac >= 3 and payback <= 18:
return "🟡 Good"
if ltv_cac >= 2 and payback <= 24:
return "🟠 Marginal"
return "🔴 Poor"
def print_cohort_analysis(results: list[UnitEconomicsResult]) -> None:
print("\n" + "="*80)
print(" COHORT ANALYSIS")
print("="*80)
print(f" {'Cohort':<12} {'Cust':>5} {'CAC':>8} {'ARPA/mo':>9} {'Churn/mo':>10} "
f"{'LTV':>10} {'LTV:CAC':>8} {'Payback':>9} {'Ret@M12':>8}")
print(" " + "-"*88)
for r in results:
payback_str = f"{r.payback_months:.1f}mo" if r.payback_months != float("inf") else "∞"
ltv_str = fmt(r.ltv) if r.ltv != float("inf") else "∞"
ltv_cac_str = f"{r.ltv_cac_ratio:.1f}x" if r.ltv_cac_ratio != float("inf") else "∞"
print(
f" {r.label:<12} {r.customers:>5} {fmt(r.cac):>8} {fmt(r.arpa):>9} "
f"{pct(r.monthly_churn):>10} {ltv_str:>10} {ltv_cac_str:>8} "
f"{payback_str:>9} {pct(r.retention_m12):>8}"
)
# Trend analysis
print("\n Cohort Trend (is the business getting better or worse?):")
if len(results) >= 3:
ltv_cac_values = [r.ltv_cac_ratio for r in results if r.ltv_cac_ratio != float("inf")]
cac_values = [r.cac for r in results]
churn_values = [r.monthly_churn for r in results]
if len(ltv_cac_values) >= 2:
ltv_cac_trend = "↑ Improving" if ltv_cac_values[-1] > ltv_cac_values[0] else "↓ Deteriorating"
else:
ltv_cac_trend = "n/a"
cac_trend = "↓ Decreasing (good)" if cac_values[-1] < cac_values[0] else "↑ Increasing"
churn_trend = "↓ Improving" if churn_values[-1] < churn_values[0] else "↑ Worsening"
print(f" LTV:CAC: {ltv_cac_trend}")
print(f" CAC: {cac_trend}")
print(f" Churn rate: {churn_trend}")
def print_channel_analysis(results: list[UnitEconomicsResult], channels: list[ChannelData]) -> None:
print("\n" + "="*80)
print(" CHANNEL ANALYSIS (Per-Channel vs Blended)")
print("="*80)
print(f" {'Channel':<22} {'Spend':>9} {'Cust':>5} {'CAC':>8} {'LTV':>10} {'LTV:CAC':>8} {'Payback':>9} {'Rating'}")
print(" " + "-"*90)
for r, ch in zip(results, channels):
payback_str = f"{r.payback_months:.1f}mo" if r.payback_months != float("inf") else "∞"
ltv_str = fmt(r.ltv) if r.ltv != float("inf") else "∞"
ltv_cac_str = f"{r.ltv_cac_ratio:.1f}x" if r.ltv_cac_ratio != float("inf") else "∞"
print(
f" {r.label:<22} {fmt(ch.spend):>9} {r.customers:>5} {fmt(r.cac):>8} "
f"{ltv_str:>10} {ltv_cac_str:>8} {payback_str:>9} {rating(r.ltv_cac_ratio, r.payback_months)}"
)
# Blended comparison
b_cac = blended_cac(channels)
b_ltv = blended_ltv(channels)
b_ltv_cac = b_ltv / b_cac if b_cac > 0 else 0
total_spend = sum(c.spend for c in channels)
total_customers = sum(c.customers_acquired for c in channels)
avg_payback = sum(
calc_payback(b_cac, c.avg_arpa, c.gross_margin_pct) * c.customers_acquired
for c in channels
) / total_customers
print(" " + "-"*90)
print(
f" {'BLENDED (dangerous)':<22} {fmt(total_spend):>9} {total_customers:>5} "
f"{fmt(b_cac):>8} {fmt(b_ltv):>10} {b_ltv_cac:.1f}x{'':<7} "
f"{avg_payback:.1f}mo{'':<4} {rating(b_ltv_cac, avg_payback)}"
)
print("\n ⚠️ Blended numbers hide channel-level problems. Manage channels individually.")
# Budget reallocation
print("\n Recommended Budget Reallocation:")
sorted_results = sorted(zip(results, channels), key=lambda x: x[0].ltv_cac_ratio, reverse=True)
for r, ch in sorted_results:
if r.ltv_cac_ratio >= 3:
action = "✅ Scale"
elif r.ltv_cac_ratio >= 2:
action = "🔄 Optimize"
else:
action = "❌ Cut / pause"
print(f" {ch.channel:<22} LTV:CAC = {r.ltv_cac_ratio:.1f}x → {action}")
def export_csv_results(cohort_results: list[UnitEconomicsResult], channel_results: list[UnitEconomicsResult]) -> str:
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["Type", "Label", "Customers", "CAC", "ARPA_Monthly", "Gross_Margin_Pct",
"Monthly_Churn", "LTV", "LTV_CAC_Ratio", "Payback_Months",
"Retention_M6", "Retention_M12"])
for r in cohort_results:
writer.writerow(["cohort", r.label, r.customers, round(r.cac, 2), round(r.arpa, 2),
r.gross_margin_pct, round(r.monthly_churn, 4),
round(r.ltv, 2) if r.ltv != float("inf") else "inf",
round(r.ltv_cac_ratio, 2) if r.ltv_cac_ratio != float("inf") else "inf",
round(r.payback_months, 2) if r.payback_months != float("inf") else "inf",
round(r.retention_m6, 3) if r.retention_m6 else "",
round(r.retention_m12, 3) if r.retention_m12 else ""])
for r in channel_results:
writer.writerow(["channel", r.label, r.customers, round(r.cac, 2), round(r.arpa, 2),
r.gross_margin_pct, round(r.monthly_churn, 4),
round(r.ltv, 2) if r.ltv != float("inf") else "inf",
round(r.ltv_cac_ratio, 2) if r.ltv_cac_ratio != float("inf") else "inf",
round(r.payback_months, 2) if r.payback_months != float("inf") else "inf",
"", ""])
return buf.getvalue()
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
def make_sample_cohorts() -> list[CohortData]:
"""
Series A SaaS company, 8 quarters of cohort data.
Shows a business improving on all dimensions over time.
"""
return [
CohortData(
label="Q1 2023", acquisition_period="Jan-Mar 2023",
customers_acquired=12, total_cac_spend=54_000,
gross_margin_pct=0.68,
monthly_revenue=[
10_200, 9_600, 9_100, 8_700, 8_300, 8_000, # M1-M6
7_800, 7_600, 7_400, 7_200, 7_000, 6_800, # M7-M12
6_700, 6_600, 6_500, 6_400, 6_300, 6_200, # M13-M18
6_100, 6_000, 5_900, 5_800, 5_700, 5_600, # M19-M24
],
),
CohortData(
label="Q2 2023", acquisition_period="Apr-Jun 2023",
customers_acquired=15, total_cac_spend=60_000,
gross_margin_pct=0.69,
monthly_revenue=[
13_500, 12_900, 12_500, 12_100, 11_800, 11_500,
11_300, 11_100, 10_900, 10_700, 10_500, 10_300,
10_200, 10_100, 10_000, 9_900, 9_800, 9_700,
],
),
CohortData(
label="Q3 2023", acquisition_period="Jul-Sep 2023",
customers_acquired=18, total_cac_spend=63_000,
gross_margin_pct=0.70,
monthly_revenue=[
16_200, 15_800, 15_400, 15_100, 14_800, 14_600,
14_400, 14_200, 14_000, 13_900, 13_800, 13_700,
13_600, 13_500, 13_400, 13_300,
],
),
CohortData(
label="Q4 2023", acquisition_period="Oct-Dec 2023",
customers_acquired=22, total_cac_spend=70_400,
gross_margin_pct=0.71,
monthly_revenue=[
20_900, 20_500, 20_200, 19_900, 19_700, 19_500,
19_300, 19_100, 19_000, 18_900, 18_800, 18_700,
],
),
CohortData(
label="Q1 2024", acquisition_period="Jan-Mar 2024",
customers_acquired=28, total_cac_spend=81_200,
gross_margin_pct=0.72,
monthly_revenue=[
27_200, 26_900, 26_600, 26_400, 26_200, 26_000,
25_800, 25_700, 25_600, 25_500,
],
),
CohortData(
label="Q2 2024", acquisition_period="Apr-Jun 2024",
customers_acquired=34, total_cac_spend=91_800,
gross_margin_pct=0.72,
monthly_revenue=[
33_300, 33_000, 32_800, 32_600, 32_400, 32_200,
],
),
CohortData(
label="Q3 2024", acquisition_period="Jul-Sep 2024",
customers_acquired=40, total_cac_spend=100_000,
gross_margin_pct=0.73,
monthly_revenue=[
39_600, 39_400, 39_200,
],
),
CohortData(
label="Q4 2024", acquisition_period="Oct-Dec 2024",
customers_acquired=47, total_cac_spend=112_800,
gross_margin_pct=0.73,
monthly_revenue=[
47_000,
],
),
]
def make_sample_channels() -> list[ChannelData]:
"""
Q4 2024 channel breakdown. Blended looks fine; per-channel reveals problems.
"""
return [
ChannelData("Organic / SEO", spend=9_500, customers_acquired=14, avg_arpa=950, gross_margin_pct=0.73, avg_monthly_churn=0.015),
ChannelData("Paid Search (SEM)", spend=48_000, customers_acquired=18, avg_arpa=980, gross_margin_pct=0.73, avg_monthly_churn=0.020),
ChannelData("Paid Social", spend=32_000, customers_acquired=8, avg_arpa=900, gross_margin_pct=0.72, avg_monthly_churn=0.025),
ChannelData("Content / Inbound", spend=11_000, customers_acquired=6, avg_arpa=1100, gross_margin_pct=0.74, avg_monthly_churn=0.012),
ChannelData("Outbound SDR", spend=22_000, customers_acquired=4, avg_arpa=1200, gross_margin_pct=0.73, avg_monthly_churn=0.022),
ChannelData("Events / Webinars", spend=18_500, customers_acquired=3, avg_arpa=1050, gross_margin_pct=0.72, avg_monthly_churn=0.028),
ChannelData("Partner / Referral", spend=7_800, customers_acquired=7, avg_arpa=1000, gross_margin_pct=0.73, avg_monthly_churn=0.013),
]
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description="Unit Economics Analyzer")
parser.add_argument("--csv", action="store_true", help="Export results as CSV to stdout")
args = parser.parse_args()
cohorts = make_sample_cohorts()
channels = make_sample_channels()
print("\n" + "="*80)
print(" UNIT ECONOMICS ANALYZER")
print(" Sample Company: Series A SaaS | Q4 2024 Snapshot")
print(" Gross Margin: ~72% | Monthly Churn: derived from cohort data")
print("="*80)
cohort_results = [analyze_cohort(c) for c in cohorts]
channel_results = [analyze_channel(c) for c in channels]
print_cohort_analysis(cohort_results)
print_channel_analysis(channel_results, channels)
# Health summary
print("\n" + "="*80)
print(" HEALTH SUMMARY")
print("="*80)
latest = cohort_results[-1]
prev = cohort_results[-4] if len(cohort_results) >= 4 else cohort_results[0]
print(f"\n Latest Cohort ({latest.label}):")
print(f" CAC: {fmt(latest.cac)}")
ltv_str = fmt(latest.ltv) if latest.ltv != float("inf") else "∞"
ltv_cac_str = f"{latest.ltv_cac_ratio:.1f}x" if latest.ltv_cac_ratio != float("inf") else "∞"
payback_str = f"{latest.payback_months:.1f} months" if latest.payback_months != float("inf") else "∞"
print(f" LTV: {ltv_str}")
print(f" LTV:CAC: {ltv_cac_str} (target: > 3x)")
print(f" CAC Payback: {payback_str} (target: < 18mo)")
print(f" Rating: {rating(latest.ltv_cac_ratio, latest.payback_months)}")
# Trend vs 4 quarters ago
print(f"\n Trend vs {prev.label}:")
cac_delta = (latest.cac - prev.cac) / prev.cac * 100
ltv_delta_str = "n/a"
if latest.ltv != float("inf") and prev.ltv != float("inf"):
ltv_delta = (latest.ltv - prev.ltv) / prev.ltv * 100
ltv_delta_str = f"{ltv_delta:+.1f}%"
cac_str = "↓ Better" if cac_delta < 0 else "↑ Worse"
print(f" CAC: {cac_delta:+.1f}% ({cac_str})")
print(f" LTV: {ltv_delta_str}")
print("\n Benchmark Reference:")
print(" LTV:CAC > 5x → Scale aggressively")
print(" LTV:CAC 3-5x → Healthy; grow at current pace")
print(" LTV:CAC 2-3x → Marginal; optimize before scaling")
print(" LTV:CAC < 2x → Acquiring unprofitably; stop and fix")
print(" Payback < 12mo → Outstanding capital efficiency")
print(" Payback 12-18mo → Good for B2B SaaS")
print(" Payback > 24mo → Requires long-dated capital to scale")
if args.csv:
print("\n\n--- CSV EXPORT ---\n")
sys.stdout.write(export_csv_results(cohort_results, channel_results))
if __name__ == "__main__":
main()
FILE:change-management/SKILL.md
---
name: "change-management"
description: "Framework for rolling out organizational changes without chaos. Covers the ADKAR model adapted for startups, communication templates, resistance patterns, and change fatigue management. Handles process changes, org restructures, strategy pivots, and culture changes. Use when announcing a reorg, switching tools, pivoting strategy, killing a product, changing leadership, or when user mentions change management, change rollout, managing resistance, org change, reorg, or pivot communication."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: change-management
updated: 2026-03-05
frameworks: change-playbook
---
# Change Management Playbook
Most changes fail at implementation, not design. The ADKAR model tells you why and how to fix it.
## Keywords
change management, ADKAR, organizational change, reorg, process change, tool migration, strategy pivot, change resistance, change fatigue, change communication, stakeholder management, adoption, compliance, change rollout, transition
## Core Model: ADKAR Adapted for Startups
ADKAR is a change management model by Prosci. Original version is for enterprises. This is the startup-speed adaptation.
### A — Awareness
**What it is:** People understand WHY the change is happening — the business reason, not just the announcement.
**The mistake:** Communicating the WHAT before the WHY. "We're moving to a new CRM" before "here's why our current process is killing us."
**What people need to hear:**
- What is the problem we're solving? (Be honest. If it's "we need to cut costs," say that.)
- Why now? What would happen if we didn't change?
- Who made this decision and how?
**Startup shortcut:** A 5-minute video from the CEO or decision-maker explaining the "why" in plain language beats a formal change announcement document every time.
---
### D — Desire
**What it is:** People want to make the change happen — or at least don't actively resist it.
**The mistake:** Assuming communication creates desire. Awareness ≠ desire. People can understand a change and still hate it.
**What creates desire:**
- "What's in it for me?" — answer this for each stakeholder group, honestly
- Involving people in the "how" even if the "what" is decided
- Addressing fears directly: "Some people are worried this means their role is changing. Here's the truth: [honest answer]"
**What destroys desire:**
- Pretending the change is better for everyone than it is
- Ignoring the legitimate losses people will experience
- Making announcements without any consultation
**Startup shortcut:** Run a short "concerns and questions" session within 48 hours of announcement. Not to reverse the decision — to address the fears and show you're listening.
---
### K — Knowledge
**What it is:** People know HOW to operate in the new world — the specific skills, behaviors, and processes.
**The mistake:** Announcing the change and assuming people will figure it out.
**What people need:**
- Step-by-step documentation of new processes
- Training or practice sessions before go-live
- Clear answers to "what do I do when [common scenario]?"
- Who to ask when they're stuck
**Types of knowledge transfer:**
| Method | Best for | When |
|--------|---------|------|
| Live training | Skill-based changes, complex tools | Before go-live |
| Documentation | Process changes, reference material | Always |
| Video walkthroughs | Tool migrations | Available 24/7, self-paced |
| Shadowing / peer learning | Behavior changes | Weeks 2–4 after launch |
| Office hours | Any change with many edge cases | First 4–6 weeks |
---
### A — Ability
**What it is:** People have the time, tools, and support to actually do things differently.
**The mistake:** "We've trained everyone" ≠ "everyone can now do it." Training is knowledge. Ability is practice.
**What creates ability:**
- Time to practice before being evaluated
- A safe environment to make mistakes (no public shaming for early struggles)
- Reduced load during transition (if you're asking people to learn new skills, don't simultaneously pile on new work)
- Access to help (a Slack channel, a point person, documentation)
**Signs of ability gap:**
- People revert to old behavior under pressure
- Workarounds emerge (people invent their own way around the new system)
- Training scores are high but actual behavior hasn't changed
---
### R — Reinforcement
**What it is:** The change sticks. The new behavior becomes the default.
**The mistake:** Declaring victory at go-live. Changes fail because they're never reinforced.
**What creates reinforcement:**
- Visible measurement (are we tracking adoption?)
- Recognition of early adopters ("Sarah fully migrated to the new workflow in week 2 — ask her how")
- Leader modeling (if the CEO uses the old way, everyone will)
- Removing the old option (when possible — eliminate the path of least resistance)
- Consequences for non-adoption (stated clearly, applied consistently)
**Adoption vs. compliance:**
- **Compliance:** People do it when watched, revert when not
- **Adoption:** People do it because they believe it's better
Only reinforcement creates adoption. Compliance is the result of enforcement. Aim for adoption.
---
## Change Types and ADKAR Application
### Process Change (new tools, new workflows)
**Timeline:** 4–8 weeks for full adoption
**Hardest phase:** Ability (people know what to do but haven't built the habit)
**Critical reinforcement:** Remove or deprecate the old tool/process
**Communication sequence:**
1. Week -2: Announce the why + go-live date
2. Week -1: Training sessions available
3. Week 0 (go-live): Launch + point person available
4. Week 2: Adoption check-in (who's using it? Who isn't?)
5. Week 4: Feedback collection + public wins
6. Week 8: Old system deprecated
---
### Org Change (reorg, new leader, team splits/merges)
**Timeline:** 3–6 months for full stabilization
**Hardest phase:** Desire (people fear for their roles and relationships)
**Critical reinforcement:** Consistent behavior from new leadership
**Communication sequence:**
1. Day 0: Announce the change with the "why" — in person or synchronous video
2. Day 1: 1:1s with most affected team members by their manager
3. Week 1: FAQ published with honest answers to the 10 most common concerns
4. Week 2–4: New structure is operating (don't delay implementation)
5. Month 2: First retrospective — what's working, what needs adjustment
6. Month 3–6: Regular check-ins on team health and morale
**What to say when a leader is leaving or being replaced:**
Be honest about what you can share. Never: "We can't share the reasons." Always: either a truthful explanation or "we're not able to share the specifics, but I can tell you [what this means for you]."
---
### Strategy Pivot (new direction, killed products)
**Timeline:** 3–12 months for full alignment
**Hardest phase:** Awareness (people don't believe the pivot is real)
**Critical reinforcement:** Resource reallocation that visibly proves the pivot is happening
**Communication sequence:**
1. Internal first, always. Employees should never hear about a pivot from a press release.
2. All-hands with full context: what changed in the market, what you're doing, what it means for teams
3. Each team leader runs a "what does this mean for us?" conversation with their team
4. Resource reallocation announced within 2 weeks (if the money doesn't move, people won't believe the pivot)
5. First milestone of the new direction celebrated publicly
**What kills pivots:** Announcing a new direction while still funding the old one at the same level.
---
### Culture Change (values refresh, behavior expectations)
**Timeline:** 12–24 months for genuine behavior change
**Hardest phase:** Reinforcement (behavior doesn't change just because values were announced)
**Critical reinforcement:** Visible decisions that reflect the new values
**Communication sequence:**
1. Build with input: involve a representative sample of the company in defining the change
2. Announce with story: "Here's what we observed, here's what we're changing and why"
3. Behavior anchors: for each culture change, state the specific behavior in observable terms
4. Leader behavior: leadership team must visibly model the new behavior first
5. Performance integration: new expected behaviors appear in reviews within one cycle
6. Celebrate the right behaviors: when someone exemplifies the new culture, name it publicly
---
## Resistance Patterns
Resistance is information, not defiance. Diagnose before responding.
| Resistance pattern | What it signals | Response |
|-------------------|-----------------|---------|
| "This won't work" | Awareness gap or credibility gap | Explain the evidence base for the change |
| "Why now?" | Awareness gap | Explain urgency — what happens if we don't change |
| "I wasn't consulted" | Desire gap | Acknowledge the gap; involve them in the "how" now |
| "I don't have time for this" | Ability gap | Reduce their load or push the timeline |
| "We tried this before" | Trust gap | Acknowledge what's different this time. Be specific. |
| Silent non-compliance | Could be any gap | 1:1 conversation to diagnose |
**The worst response to resistance:** Dismissing it. "Some people are resistant to change" as if resistance is a personality flaw rather than a signal.
---
## Change Fatigue
When organizations change too fast, people stop believing any change will stick.
### Signals
- Eye-rolls during change announcements ("here we go again")
- Low attendance at change-related sessions
- Fast compliance on paper, slow adoption in practice
- "Last month we were doing X, now we're doing Y" comments
### Prevention
- **Finish what you start.** Don't announce a new change while the last one is still being absorbed.
- **Space changes.** One significant change at a time. Give 2–3 months of stability between major changes.
- **Announce what's NOT changing.** People in change-fatigue need to know what's stable.
- **Show results.** Publish what the previous change achieved before launching the next.
### When you're already in change fatigue
- Pause non-critical changes
- Run a "change inventory": how many changes are in progress simultaneously?
- Prioritize ruthlessly: which changes are essential now? Which can wait?
- Communicate stability: "Here's what is NOT changing this quarter"
---
## Key Questions for Change Management
- "Who are the most skeptical people about this change? Have we talked to them directly?"
- "Do people understand why we're doing this, or just what we're doing?"
- "Have we given people time to practice before we measure performance on the new way?"
- "Is the old way still available? If so, people will use it."
- "Are leaders modeling the new behavior themselves?"
- "How many changes are we running simultaneously right now?"
## Red Flags
- Change announced on Friday afternoon (people stew over the weekend)
- "This is final, questions are not welcome" framing
- No published FAQ or way to ask questions safely
- Old system/process still running 6 weeks after "go-live"
- Leaders exempted from the change they're asking everyone else to make
- No measurement of adoption — assuming go-live = success
## Detailed References
- `references/change-playbook.md` — ADKAR deep dive, resistance counter-strategies, communication templates, change fatigue management
FILE:change-management/references/change-playbook.md
# Change Management Playbook
Deep reference for rolling out organizational changes effectively.
---
## 1. ADKAR Deep Dive with Startup Examples
### Awareness: The "Why" that actually lands
Most change communications fail at awareness because they confuse informing with explaining.
**Informing:** "We're moving from Jira to Linear next month."
**Explaining:** "Our engineering team loses ~4 hours per week to Jira configuration, search latency, and reporting setup. At our current team size, that's 60+ hours per month. Linear's benchmarks from teams our size show a 40% reduction in that overhead. That's why we're switching — and here's the timeline."
The explanation activates desire. The announcement just creates work.
**Real example: Tool migration**
> "We tried Asana, we tried Notion tasks, we tried spreadsheets. None of them stuck. After talking to 8 engineering leads at similar companies, the pattern was clear: teams that use Linear stick with it. We're going all-in. Here's why it will be different this time: [specific reasons]."
**Real example: Reorg**
> "The current structure has our customer success team reporting to Sales, which creates a conflict: Sales is measured on new logo count, CS is measured on retention. We've seen this play out in three recent customer losses where CS needed to raise concerns but felt the pressure to stay quiet. We're changing the reporting structure so CS reports directly to me. This is about removing a structural conflict, not about performance."
---
### Desire: Addressing the "What's in it for me?"
Every stakeholder group needs a different answer.
**Individual contributor:**
- "Will my job change significantly?"
- "Will this make my day easier or harder?"
- "Is my role at risk?"
**Manager:**
- "What new responsibilities do I take on?"
- "How do I explain this to my team?"
- "What happens if someone on my team doesn't adapt?"
**Senior leader:**
- "What does this change our strategic posture?"
- "What resources are reallocated and to what?"
- "How does this affect my relationships with other senior leaders?"
**Resistance scenario: Senior leader whose team is most affected**
> They're supportive in the room, silent or undermining outside it.
> Fix: Give them a role in the change. Make them a named co-leader of the implementation. Invested people don't undermine.
---
### Knowledge: The documentation that actually gets used
The reason most change documentation fails: it's written for the decision-maker, not the user.
**Documentation that gets used:**
- Short (< 2 pages for most changes)
- Organized by role: "If you're in Sales, here's what changes for you"
- Answers "what do I do when X happens?" with specific answers
- Has a clear owner: "Questions? Ask [person] in #channel"
**Documentation that doesn't get used:**
- Long rationale sections the user doesn't need
- "See the full policy document for details"
- No named point of contact
- Buried in email threads
---
### Ability: The gap between knowing and doing
Signs of a knowledge gap vs. an ability gap:
| Symptom | Knowledge gap | Ability gap |
|---------|-------------|------------|
| People don't know what to do | ✅ | |
| People know what to do but don't do it | | ✅ |
| People do it wrong consistently | Could be either | |
| People revert under pressure | | ✅ |
| Training scores high, behavior unchanged | | ✅ |
**Ability gaps are fixed by:**
1. Practice time (before being measured)
2. Reduced cognitive load during transition
3. Peer support (not just manager support)
4. Feedback loops that are fast and low-stakes
**What kills ability development:**
- Measuring performance on the new way in week 1
- Adding new work simultaneously with the change
- Making it embarrassing to ask for help
---
### Reinforcement: The phase everyone skips
Go-live is not success. Go-live is the beginning of adoption.
**Reinforcement calendar (template):**
| Week | Action |
|------|--------|
| Week 1 (go-live) | High-visibility support. Leadership visible. Point person responsive. |
| Week 2 | First adoption check: who's using it? Who isn't? Targeted help to laggards. |
| Week 4 | Celebrate early adopters publicly. Share a win story. |
| Week 6 | Adoption metric reported to leadership. Decommission old way (if applicable). |
| Week 8 | Full adoption expected. Non-adoption now a performance conversation. |
| Month 3 | Retrospective: What's working? What needs adjustment? |
---
## 2. Resistance Patterns and Counter-Strategies
### The Vocal Skeptic
**Who they are:** Asks hard questions in all-hands. Other people follow their lead.
**What they need:** To feel heard and to understand the logic.
**Strategy:** Talk to them before the all-hands. Not to persuade them — to hear their concerns and address what's valid. When they feel respected, they often become your best change advocates.
**Script:** "I know you have concerns about this change. I want to understand them before we go broader with the announcement. What's your biggest worry?"
---
### The Silent Non-Complier
**Who they are:** Agrees in meetings, continues the old behavior outside.
**What they need:** To understand that non-compliance is visible and has consequences.
**Strategy:** Direct 1:1 conversation. Name the behavior. Ask what's in the way. Give them a clear path.
**Script:** "I've noticed you're still using [old way] two weeks after we launched [new way]. I want to understand what's in the way for you — is it a knowledge issue, a time issue, or something else?"
---
### The Grieving Top Performer
**Who they are:** Was excellent under the old system. The change makes their skills less relevant.
**What they need:** Recognition of their past contribution and a clear path forward.
**Strategy:** Name the loss explicitly. "I know you built your expertise on [old approach] and this change asks you to develop a new one. That's a real transition." Then create a specific development plan.
**What not to do:** Pretend the change doesn't affect them disproportionately.
---
### The Fearful Middle Manager
**Who they are:** Middle managers whose authority or role scope is reduced by the change.
**What they need:** A clear picture of their new role and why it's still valuable.
**Strategy:** Individual conversation before the announcement. Walk them through what changes, what stays the same, and what their contribution looks like in the new world.
---
### The "We've Been Here Before" Cynics
**Who they are:** Long-tenured employees who've seen multiple failed change initiatives.
**What they need:** Evidence that this time is different.
**Strategy:** Acknowledge the history. "I know we've announced changes that didn't stick. Here's specifically what's different this time: [specific differences]." Then prove it fast — show momentum in the first 30 days.
---
## 3. Communication Plan Template per Change Type
### Template: Tool Migration
```
COMMUNICATION PLAN — [Tool Name] Migration
AUDIENCE: All-hands / [specific team]
DECISION OWNER: [Name]
GO-LIVE DATE: [Date]
POINT OF CONTACT: [Name] in [channel]
COMMUNICATION TIMELINE:
Week -4: Decision finalized (internal only)
Week -3: Training materials ready
Week -2: All-hands announcement (why + timeline + support plan)
Week -1: Training sessions (2 sessions, different times)
Week 0: Go-live. Point person in Slack. Old system still accessible.
Week 2: First adoption check. Targeted help to non-adopters.
Week 4: Old system access restricted.
Week 8: Old system fully decommissioned.
KEY MESSAGES:
- Why we're switching: [honest 2-sentence reason]
- What changes for you: [role-specific, max 3 bullets]
- What doesn't change: [this matters for change fatigue]
- How to get help: [channel, person, office hours]
- Timeline: [specific dates]
FAQ:
Q: Is the old system going away completely?
A: [Honest answer with date]
Q: What if I have data in the old system?
A: [Migration plan or acknowledgment]
Q: What if I'm not proficient by go-live?
A: [Realistic expectation-setting]
```
### Template: Reorg Announcement
```
REORG COMMUNICATION PLAN
ANNOUNCEMENT DATE: [Date]
EFFECTIVE DATE: [Date]
FORMAT: Live (synchronous), all affected employees
PRE-ANNOUNCEMENT (1 week before):
- 1:1 with every affected leader
- HR briefed and ready for questions
- FAQ prepared
ANNOUNCEMENT FORMAT:
1. Context: Why this change? (2-3 minutes)
2. What's changing: New structure, new reporting lines (3-4 minutes)
3. What's NOT changing: Roles, comp, team members (2 minutes)
4. Timeline: When does the new structure take effect? (1 minute)
5. Q&A: Open, no time limit (at least 15 minutes)
POST-ANNOUNCEMENT (week 1):
- Each manager runs team meeting to answer team-specific questions
- HR available for private conversations
- FAQ published to all
POST-ANNOUNCEMENT (week 2-4):
- New structure is operational
- Transition check-in: what questions emerged that weren't anticipated?
THINGS NOT TO SAY:
- "We can't share why [person] is leaving" (if they are)
- "This affects everyone equally" (it doesn't)
- "No one's job is at risk" (unless this is 100% certain)
```
---
## 4. The Change Fatigue Problem
### How organizations develop change fatigue
**Phase 1 — Excitement (first 1-2 changes):** People engage, try the new way, hope it sticks.
**Phase 2 — Skepticism (3-5 changes):** People comply but hedge. "Let's see if this one lasts."
**Phase 3 — Detachment (6+ changes without completion):** People stop investing in changes. Compliance is surface-level. New announcements get eye-rolls.
**Phase 4 — Cynicism (entrenched fatigue):** People actively resist changes. "We've been here before." High performers leave because they don't want to work in a chaotic environment.
### The change inventory audit
**Run this before announcing any new change:**
| Change | Status | Started | Expected complete |
|--------|--------|---------|-----------------|
| [Change 1] | In progress / Complete / Stalled | | |
| [Change 2] | | | |
| [Change 3] | | | |
**Rules:**
- If > 2 significant changes are in progress, don't start a third
- If any change is stalled, diagnose it before starting something new
- Define "complete" for every change in progress
### Recovery from change fatigue
1. **Declare a change moratorium.** "We're not starting anything new for 60 days. We're finishing what we started."
2. **Complete visible wins.** Ship the changes that are 80% done. Demonstrate follow-through.
3. **Communicate stability.** "Here's what is NOT changing this year."
4. **Slow down the next announcement.** More preparation, more consultation, clearer "this time is different" evidence.
---
## 5. Measuring Adoption vs. Compliance
Most change leaders measure go-live, not adoption. These are different things.
### Adoption metrics by change type
**Tool migration:**
- % of team actively using the new tool (not just logged in)
- % of relevant workflows completed in new tool vs. old tool
- Support ticket volume in weeks 1-4 (high = knowledge gap; dropping = adoption)
**Process change:**
- % of relevant transactions following new process
- Error rates in new process vs. old process (should converge over time)
- Time-to-complete for new process (should improve by week 4)
**Org change:**
- Decision cycle time in new structure (should improve by month 2)
- Escalation patterns (fewer cross-boundary escalations = alignment improving)
- Employee sentiment (survey at months 1, 3, 6)
**Culture change:**
- Values referenced in 1:1 conversations (manager self-report)
- Values-linked recognition events per month
- Culture survey scores in relevant dimensions (quarterly)
### The compliance trap
Measuring compliance: "Did they use the new system? Yes/No."
Measuring adoption: "Did they use the new system because it's better, or because they had to?"
Compliance is unstable. It reverts when enforcement loosens. Adoption is self-sustaining.
**Adoption diagnostic:** Ask a random sample: "Why do you use [new way] instead of [old way]?"
- "Because I have to" = compliance
- "Because it's faster/easier/better" = adoption
Only adoption makes the change permanent.
FILE:chief-of-staff/SKILL.md
---
name: "chief-of-staff"
description: "C-suite orchestration layer. Routes founder questions to the right advisor role(s), triggers multi-role board meetings for complex decisions, synthesizes outputs, and tracks decisions. Every C-suite interaction starts here. Loads company context automatically."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: orchestration
updated: 2026-03-05
frameworks: routing-matrix, synthesis-framework, decision-log, board-protocol
---
# Chief of Staff
The orchestration layer between founder and C-suite. Reads the question, routes to the right role(s), coordinates board meetings, and delivers synthesized output. Loads company context for every interaction.
## Keywords
chief of staff, orchestrator, routing, c-suite coordinator, board meeting, multi-agent, advisor coordination, decision log, synthesis
---
## Session Protocol (Every Interaction)
1. Load company context via context-engine skill
2. Score decision complexity
3. Route to role(s) or trigger board meeting
4. Synthesize output
5. Log decision if reached
---
## Invocation Syntax
```
[INVOKE:role|question]
```
Examples:
```
[INVOKE:cfo|What's the right runway target given our growth rate?]
[INVOKE:board|Should we raise a bridge or cut to profitability?]
```
### Loop Prevention Rules (CRITICAL)
1. **Chief of Staff cannot invoke itself.**
2. **Maximum depth: 2.** Chief of Staff → Role → stop.
3. **Circular blocking.** A→B→A is blocked. Log it.
4. **Board = depth 1.** Roles at board meeting do not invoke each other.
If loop detected: return to founder with "The advisors are deadlocked. Here's where they disagree: [summary]."
---
## Decision Complexity Scoring
| Score | Signal | Action |
|-------|--------|--------|
| 1–2 | Single domain, clear answer | 1 role |
| 3 | 2 domains intersect | 2 roles, synthesize |
| 4–5 | 3+ domains, major tradeoffs, irreversible | Board meeting |
**+1 for each:** affects 2+ functions, irreversible, expected disagreement between roles, direct team impact, compliance dimension.
---
## Routing Matrix (Summary)
Full rules in `references/routing-matrix.md`.
| Topic | Primary | Secondary |
|-------|---------|-----------|
| Fundraising, burn, financial model | CFO | CEO |
| Hiring, firing, culture, performance | CHRO | COO |
| Product roadmap, prioritization | CPO | CTO |
| Architecture, tech debt | CTO | CPO |
| Revenue, sales, GTM, pricing | CRO | CFO |
| Process, OKRs, execution | COO | CFO |
| Security, compliance, risk | CISO | COO |
| Company direction, investor relations | CEO | Board |
| Market strategy, positioning | CMO | CRO |
| M&A, pivots | CEO | Board |
---
## Board Meeting Protocol
**Trigger:** Score ≥ 4, or multi-function irreversible decision.
```
BOARD MEETING: [Topic]
Attendees: [Roles]
Agenda: [2–3 specific questions]
[INVOKE:role1|agenda question]
[INVOKE:role2|agenda question]
[INVOKE:role3|agenda question]
[Chief of Staff synthesis]
```
**Rules:** Max 5 roles. Each role one turn, no back-and-forth. Chief of Staff synthesizes. Conflicts surfaced, not resolved — founder decides.
---
## Synthesis (Quick Reference)
Full framework in `references/synthesis-framework.md`.
1. **Extract themes** — what 2+ roles agree on independently
2. **Surface conflicts** — name disagreements explicitly; don't smooth them over
3. **Action items** — specific, owned, time-bound (max 5)
4. **One decision point** — the single thing needing founder judgment
**Output format:**
```
## What We Agree On
[2–3 consensus themes]
## The Disagreement
[Named conflict + each side's reasoning + what it's really about]
## Recommended Actions
1. [Action] — [Owner] — [Timeline]
...
## Your Decision Point
[One question. Two options with trade-offs. No recommendation — just clarity.]
```
---
## Decision Log
Track decisions to `~/.claude/decision-log.md`.
```
## Decision: [Name]
Date: [YYYY-MM-DD]
Question: [Original question]
Decided: [What was decided]
Owner: [Who executes]
Review: [When to check back]
```
At session start: if a review date has passed, flag it: *"You decided [X] on [date]. Worth a check-in?"*
---
## Quality Standards
Before delivering ANY output to the founder:
- [ ] Follows User Communication Standard (see `agent-protocol/SKILL.md`)
- [ ] Bottom line is first — no preamble, no process narration
- [ ] Company context loaded (not generic advice)
- [ ] Every finding has WHAT + WHY + HOW
- [ ] Actions have owners and deadlines (no "we should consider")
- [ ] Decisions framed as options with trade-offs and recommendation
- [ ] Conflicts named, not smoothed
- [ ] Risks are concrete (if X → Y happens, costs $Z)
- [ ] No loops occurred
- [ ] Max 5 bullets per section — overflow to reference
---
## Ecosystem Awareness
The Chief of Staff routes to **28 skills total**:
- **10 C-suite roles** — CEO, CTO, COO, CPO, CMO, CFO, CRO, CISO, CHRO, Executive Mentor
- **6 orchestration skills** — cs-onboard, context-engine, board-meeting, decision-logger, agent-protocol
- **6 cross-cutting skills** — board-deck-builder, scenario-war-room, competitive-intel, org-health-diagnostic, ma-playbook, intl-expansion
- **6 culture & collaboration skills** — culture-architect, company-os, founder-coach, strategic-alignment, change-management, internal-narrative
See `references/routing-matrix.md` for complete trigger mapping.
## References
- `references/routing-matrix.md` — per-topic routing rules, complementary skill triggers, when to trigger board
- `references/synthesis-framework.md` — full synthesis process, conflict types, output format
FILE:chief-of-staff/references/routing-matrix.md
# Routing Matrix
Detailed routing rules for the Chief of Staff. When a founder asks a question, find the best match in this matrix, then apply the scoring rules to determine single-role, multi-role, or board meeting.
---
## Routing by Domain
### Finance & Capital
| Question type | Primary | Secondary | Score |
|--------------|---------|-----------|-------|
| How much runway do we have? | CFO | — | 1 |
| Should we raise now or later? | CFO | CEO | 3 |
| What's our burn multiple? | CFO | COO | 2 |
| Should we raise a bridge or cut costs? | CFO | CEO, COO | 5 |
| What's the right pricing model? | CFO | CRO, CPO | 4 |
| Should we hire or extend runway? | CFO | CHRO, COO | 4 |
| What terms should we accept for this round? | CFO | CEO | 3 |
| How do we model the next 18 months? | CFO | COO | 2 |
### People & Culture
| Question type | Primary | Secondary | Score |
|--------------|---------|-----------|-------|
| Should I let this person go? | CHRO | COO | 2 |
| How do I structure comp for the team? | CHRO | CFO | 3 |
| We have a culture problem — what do we do? | CHRO | CEO | 3 |
| A leader on my team isn't working — now what? | CHRO | COO | 2 |
| How do I hire fast without breaking culture? | CHRO | COO | 3 |
| Two co-founders are in conflict | CHRO | CEO | 4 |
| How do we retain our best people? | CHRO | CFO | 2 |
| What does a good performance management process look like? | CHRO | COO | 2 |
### Product
| Question type | Primary | Secondary | Score |
|--------------|---------|-----------|-------|
| What should we build next? | CPO | CTO | 2 |
| Should we kill this feature? | CPO | CTO, CRO | 3 |
| How do we prioritize the roadmap? | CPO | CTO, COO | 3 |
| Are we pre-PMF or post-PMF? | CPO | CRO, CEO | 4 |
| Should we build vs buy? | CPO | CTO, CFO | 4 |
| How do we handle technical debt vs new features? | CTO | CPO | 3 |
| What's our product strategy for next year? | CPO | CEO, CRO | 4 |
### Technology & Engineering
| Question type | Primary | Secondary | Score |
|--------------|---------|-----------|-------|
| What architecture should we use? | CTO | CPO | 1 |
| How do we scale the system to 10x traffic? | CTO | COO | 2 |
| We have a security incident — what now? | CISO | CTO, COO | 5 |
| Should we migrate to microservices? | CTO | COO, CFO | 4 |
| How do I grow the engineering team? | CTO | CHRO, CFO | 3 |
| Our engineering velocity is dropping — why? | CTO | COO | 2 |
| What's our DevOps maturity? | CTO | COO | 1 |
| How do we handle a compliance audit on our tech? | CISO | CTO | 3 |
### Sales & Revenue
| Question type | Primary | Secondary | Score |
|--------------|---------|-----------|-------|
| Why aren't we closing deals? | CRO | CPO | 2 |
| How do we build a sales process from scratch? | CRO | COO | 2 |
| What's the right GTM for this market? | CRO | CMO, CEO | 4 |
| Our churn is too high — root cause? | CRO | CPO, CHRO | 3 |
| Should we go enterprise or stay SMB? | CRO | CPO, CFO | 4 |
| How do we expand into a new market? | CRO | CMO, CEO, CFO | 5 |
| What's our ideal customer profile? | CRO | CPO, CMO | 3 |
| Pipeline is dry — what do we do? | CRO | CMO | 2 |
### Operations & Execution
| Question type | Primary | Secondary | Score |
|--------------|---------|-----------|-------|
| Why do things keep breaking? | COO | CTO | 2 |
| How do we set up OKRs? | COO | CEO | 2 |
| Our meetings are useless — fix it | COO | — | 1 |
| How do we scale operations without hiring? | COO | CTO, CFO | 3 |
| There's a recurring bottleneck — how to fix it? | COO | CTO | 2 |
| We need a cross-team process for X | COO | Relevant dept head | 2 |
| How do we improve decision speed? | COO | CEO | 3 |
### Marketing & Brand
| Question type | Primary | Secondary | Score |
|--------------|---------|-----------|-------|
| How do we position against Competitor X? | CMO | CRO | 2 |
| What channels should we invest in? | CMO | CRO, CFO | 3 |
| Our brand isn't resonating — why? | CMO | CPO, CRO | 3 |
| How do we build a content strategy? | CMO | CRO | 2 |
| What's our marketing budget allocation? | CMO | CFO, CRO | 3 |
### Security & Compliance
| Question type | Primary | Secondary | Score |
|--------------|---------|-----------|-------|
| How do we pass an ISO 27001 audit? | CISO | COO | 2 |
| We had a data breach — what now? | CISO | CTO, CEO, COO | 5 |
| How do we handle GDPR compliance? | CISO | CTO | 2 |
| What's our security posture? | CISO | CTO | 1 |
| A regulator is asking questions | CISO | CEO, COO | 4 |
### Strategic Direction
| Question type | Primary | Secondary | Score |
|--------------|---------|-----------|-------|
| Should we pivot? | CEO | Board meeting | 5 |
| Are we building the right company? | CEO | Board meeting | 5 |
| How do we handle an acquisition offer? | CEO | CFO, Board meeting | 5 |
| What's the 3-year strategy? | CEO | All C-suite, board | 5 |
| Should we enter a new vertical? | CEO | CRO, CFO, CPO | 4 |
---
## When to Invoke Multiple Roles
Invoke 2 roles when:
- The question sits at the boundary of two domains
- One role's answer creates a constraint the other needs to know about
- The founder explicitly wants two perspectives
Invoke 3+ roles (board) when:
- The question involves irreversible resource commitment
- There's a known tension between functions (e.g., product vs revenue, speed vs quality)
- The answer will change how multiple teams operate
- It's a company-direction question, not an operational one
---
## When NOT to Invoke Multiple Roles
Don't multi-invoke when:
- The answer is technical and one role clearly owns it
- The founder just needs a framework, not a decision
- Invoking more roles would add noise without adding signal
- Time is short and a directional answer beats a comprehensive one
---
## Escalation Criteria → Board Meeting
Automatically escalate to board meeting when any of these apply:
1. **Irreversibility:** The decision is hard or impossible to reverse (layoffs, pivots, major contracts, fundraising terms)
2. **Cross-functional resource impact:** The decision changes budget, headcount, or priorities for 2+ teams
3. **Founder blind spot risk:** The topic is in an area where the founder's archetype creates a known gap (e.g., technical founder on GTM)
4. **Disagreement expected:** The domains involved are known to have competing incentives (CFO vs CRO on pricing, CTO vs CPO on tech debt)
5. **Explicit request:** Founder says "what does the team think" or "I want multiple perspectives"
6. **Score ≥ 4**
---
## Role Registry
| Role | File | Domain |
|------|------|--------|
| CEO | ceo-advisor | Strategy, culture, investor relations |
| CFO | cfo-advisor | Finance, capital, unit economics |
| COO | coo-advisor | Operations, OKRs, scaling |
| CTO | cto-advisor | Engineering, architecture, tech strategy |
| CPO | cpo-advisor | Product, roadmap, UX |
| CRO | cro-advisor | Revenue, sales, GTM |
| CMO | cmo-advisor | Marketing, brand, positioning |
| CHRO | chro-advisor | People, culture, hiring |
| CISO | ciso-advisor | Security, compliance, risk |
**If a role file doesn't exist:** Note the gap. Answer from first principles with domain expertise. Log that the role is missing.
---
## Complementary Skills Registry
These skills are invoked for specific cross-cutting needs, not for general domain questions.
### Orchestration & Infrastructure
| Skill | Trigger | File |
|-------|---------|------|
| C-Suite Onboard | `/cs:setup`, first-time setup, "tell me about your company" | cs-onboard |
| Context Engine | Auto-loaded; staleness check | context-engine |
| Board Meeting | `/cs:board`, multi-role decisions, score ≥ 4 | board-meeting |
| Decision Logger | After board meetings, `/cs:decisions`, `/cs:review` | decision-logger |
| Agent Protocol | Inter-role invocations, loop detection | agent-protocol |
### Cross-Cutting Capabilities
| Skill | Trigger | File |
|-------|---------|------|
| Board Deck Builder | "board deck", "investor update", "board presentation" | board-deck-builder |
| Scenario War Room | "what if", multi-variable scenarios, stress test across functions | scenario-war-room |
| Competitive Intelligence | "competitor", "competitive analysis", "battlecard", "who's winning" | competitive-intel |
| Org Health Diagnostic | "how healthy are we", "org health", "company health check" | org-health-diagnostic |
| M&A Playbook | "acquisition", "M&A", "due diligence", "being acquired" | ma-playbook |
| International Expansion | "expand to", "new market", "international", "localization" | intl-expansion |
### Culture & Collaboration
| Skill | Trigger | File |
|-------|---------|------|
| Culture Architect | "values", "culture", "mission", "vision", culture problems | culture-architect |
| Company OS | "operating system", "EOS", "Scaling Up", "meeting cadence", "how do we run" | company-os |
| Founder Coach | "delegation", "blind spots", "founder growth", "leadership style", burnout | founder-coach |
| Strategic Alignment | "alignment", "silos", "teams not aligned", "strategy cascade" | strategic-alignment |
| Change Management | "rolling out", "reorg", "change", "new process", "transition" | change-management |
| Internal Narrative | "all-hands", "internal comms", "how do we tell", "narrative" | internal-narrative |
### Routing Priority
1. Check if it matches a **complementary skill trigger** → route there
2. Check if it matches a **single role domain** → route to that role
3. Check if it spans **multiple role domains** (score ≥ 3) → invoke multiple roles
4. Check if it meets **escalation criteria** (score ≥ 4 or irreversible) → trigger board meeting
5. If unclear → ask one clarifying question, then route
FILE:chief-of-staff/references/synthesis-framework.md
# Synthesis Framework
How to turn multiple role outputs into a single, useful response for the founder. Synthesis is the highest-value function of the Chief of Staff — it's not about summarizing, it's about integrating.
---
## The Problem with Multi-Role Output
Without synthesis, multiple advisors produce noise:
- Overlapping advice
- Contradictions without resolution
- Action items from every role that compete for priority
- Founder left to figure out what to do with it all
Synthesis turns this into signal: one clear picture, explicit conflicts named, prioritized actions, one decision point.
---
## Phase 1: Collect and Read
Before writing anything, read all role responses completely. Look for:
**Consensus signals:**
- Same recommendation from 2+ roles independently
- Same risk identified from different angles
- Same root cause named without coordination
**Conflict signals:**
- One role says X, another says not-X
- Same data interpreted differently
- Competing resource requests (CFO says cut costs, CRO says invest in sales)
- Different time horizons (CTO wants to fix tech debt now, CPO wants to ship features now)
**Gap signals:**
- A critical dimension no role addressed
- A risk nobody flagged
- An assumption baked in that nobody questioned
---
## Phase 2: Extract Themes
A theme is a finding that appears in 2+ role responses, even if framed differently.
**How to identify:**
1. List every distinct point from every role response
2. Group points that are about the same underlying issue
3. Name the group with a clear, plain-language label
4. Note which roles contributed to it
**Example:**
> CFO: "The burn multiple is 3.2 — unsustainable without revenue acceleration."
> CRO: "We need 3 more sales cycles to hit targets, minimum 90 days."
> COO: "Three positions are open that will cost $40K/month when filled."
>
> Theme: **Cash position is tighter than the headline number suggests.** (CFO + CRO + COO)
**Limit to 3 themes.** More than 3 means you're not synthesizing — you're listing.
---
## Phase 3: Surface Conflicts
Name every conflict explicitly. Don't resolve it — present it.
**Conflict types:**
### Resource conflict
Two roles want the same budget, headcount, or time.
> "CFO wants to delay the new hire until Q3. CHRO says the team is already at capacity and another quarter will cause attrition. Both are right from their domain."
### Priority conflict
Two roles disagree on what's most important right now.
> "CTO wants 6 weeks on infrastructure to prevent outages. CPO wants those same engineers on the new feature for the sales pipeline. This isn't a technical question — it's a risk tolerance question."
### Time horizon conflict
Two roles are optimizing for different time frames.
> "CRO is optimizing for this quarter's close rate. CMO is optimizing for brand that compounds over 18 months. Both strategies are valid. They require different budget allocations."
### Assumption conflict
Two roles have incompatible assumptions baked in.
> "CFO's model assumes 15% MoM growth. CRO says realistic growth is 8% given the sales cycle length. The financial model needs to be rebuilt on the CRO's number."
**Present conflicts without picking sides.** The founder decides which trade-off to accept.
---
## Phase 4: Derive Action Items
From the consensus themes and the non-conflicting role outputs, derive concrete actions.
**Action item criteria:**
- Specific (not "improve the process" — "map the QA process and find the bottleneck")
- Owned (assign to a role or person)
- Time-bound (this week / this quarter / before next board)
- Consequence-linked (why does it matter if it slips)
**Good example:**
> **Action:** Build an updated 18-month financial model using CRO's 8% MoM growth assumption.
> **Owner:** CFO
> **By:** End of week
> **Why it matters:** Current fundraising conversations are based on a model that's too optimistic.
**Bad example:**
> Review the financial model with the team.
**Limit to 5 actions.** If there are more, prioritize by impact and flag the rest as backlog.
---
## Phase 5: Identify the Founder Decision Point
Every board meeting ends with one question for the founder. Just one.
**How to find it:**
- It's usually the conflict that can't be resolved without a values choice
- It's the question where both sides have a legitimate case
- It's the thing none of the advisors can decide unilaterally
**Frame it cleanly:**
> "The C-suite is aligned on the actions above, but there's one thing that needs your call: [specific decision]. [Role A] recommends X because [reason]. [Role B] recommends Y because [reason]. This is ultimately a question of [underlying trade-off — growth vs profitability / speed vs stability / short-term vs long-term]."
**Don't present multiple decision points.** Force the synthesis down to one. If there are genuinely two unrelated decisions, separate them into two outputs.
---
## Output Format
```markdown
## [Topic] — C-Suite Synthesis
### What We Agree On
[Theme 1 with 1–2 sentences]
[Theme 2 with 1–2 sentences]
[Theme 3 with 1–2 sentences]
### The Disagreement
[Name the conflict]
[Role A position + reasoning]
[Role B position + reasoning]
[What the conflict is really about]
### Recommended Actions
1. **[Action]** — [Owner] — [Timeline] — [Why it matters]
2. **[Action]** — [Owner] — [Timeline]
3. **[Action]** — [Owner] — [Timeline]
4. **[Action]** — [Owner] — [Timeline]
5. **[Action]** — [Owner] — [Timeline]
### Your Decision Point
[One question for the founder. Two options with their trade-offs. No recommendation — just clarity.]
```
---
## Quality Standards for Synthesis
Before delivering:
**Compression test:** Could a founder read this in 3 minutes and know exactly what to do? If not, cut.
**Honesty test:** Did you name the real conflicts, or smooth them over? Smoothed conflicts come back as surprises.
**Specificity test:** Are the action items specific enough to act on, or are they goals masquerading as actions?
**Decision point test:** Is there one clear thing for the founder to decide, or are you leaving them with a mess?
**Context test:** Would this advice make sense for any company, or is it clearly calibrated to this company's stage, challenges, and founder?
---
## Common Synthesis Failures
**The summary trap:** You summarize each role's output in sequence. This is not synthesis — it's transcription. Synthesis requires cutting.
**The false consensus:** You say "the team agrees" when there's actually a meaningful conflict. Named conflicts are useful. Hidden conflicts are dangerous.
**The advice avalanche:** 15 action items that no one can action. Cut to 5. If everything is priority, nothing is.
**The unresolved conflict dump:** You present the conflict and then leave the founder to figure it out. Your job is to frame the choice cleanly, not to resolve it — but also not to dump it raw.
**The context-free advice:** The synthesis sounds like it came from a textbook, not from someone who knows this company. If you can swap the company name and it still reads the same, it's not synthesized.
---
## When Synthesis Reveals Deadlock
Sometimes roles genuinely can't align and the synthesis produces no clear direction.
**Signs of deadlock:**
- Every theme has a counter-theme
- Every action has a conflict attached
- The "decision point" is actually three decisions
**What to do:**
1. Name the deadlock explicitly: *"The C-suite is genuinely split on this. Here's why."*
2. Present the two paths cleanly with consequences
3. Recommend a time-boxed experiment if possible: *"You don't have to decide between X and Y permanently. Run X for 30 days with a clear metric for success, then reassess."*
4. Flag it as a strategic question that may need external input (advisor, board, market data)
Deadlock is honest. Fake consensus is not.
FILE:chro-advisor/SKILL.md
---
name: "chro-advisor"
description: "People leadership for scaling companies. Hiring strategy, compensation design, org structure, culture, and retention. Use when building hiring plans, designing comp frameworks, restructuring teams, managing performance, building culture, or when user mentions CHRO, HR, people strategy, talent, headcount, compensation, org design, retention, or performance management."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: chro-leadership
updated: 2026-03-05
python-tools: hiring_plan_modeler.py, comp_benchmarker.py
frameworks: people-strategy, comp-frameworks, org-design
---
# CHRO Advisor
People strategy and operational HR frameworks for business-aligned hiring, compensation, org design, and culture that scales.
## Keywords
CHRO, chief people officer, CPO, HR, human resources, people strategy, hiring plan, headcount planning, talent acquisition, recruiting, compensation, salary bands, equity, org design, organizational design, career ladder, title framework, retention, performance management, culture, engagement, remote work, hybrid, spans of control, succession planning, attrition
## Quick Start
```bash
python scripts/hiring_plan_modeler.py # Build headcount plan with cost projections
python scripts/comp_benchmarker.py # Benchmark salaries and model total comp
```
## Core Responsibilities
### 1. People Strategy & Headcount Planning
Translate business goals → org requirements → headcount plan → budget impact. Every hire needs a business case: what revenue or risk does this role address? See `references/people_strategy.md` for hiring at each growth stage.
### 2. Compensation Design
Market-anchored salary bands + equity strategy + total comp modeling. See `references/comp_frameworks.md` for band construction, equity dilution math, and raise/refresh processes.
### 3. Org Design
Right structure for the stage. Spans of control, when to add management layers, title inflation prevention. See `references/org_design.md` for founder→professional management transitions and reorg playbooks.
### 4. Retention & Performance
Retention starts at hire. Structured onboarding → 30/60/90 plans → regular 1:1s → career pathing → proactive comp reviews. See `references/people_strategy.md` for what actually moves the needle.
**Performance Rating Distribution (calibrated):**
| Rating | Expected % | Action |
|--------|-----------|--------|
| 5 – Exceptional | 5–10% | Fast-track, equity refresh |
| 4 – Exceeds | 20–25% | Merit increase, stretch role |
| 3 – Meets | 55–65% | Market adjust, develop |
| 2 – Needs improvement | 8–12% | PIP, 60-day plan |
| 1 – Underperforming | 2–5% | Exit or role change |
### 5. Culture & Engagement
Culture is behavior, not values on a wall. Measure eNPS quarterly. Act on results within 30 days or don't ask.
## Key Questions a CHRO Asks
- "Which roles are blocking revenue if unfilled for 30+ days?"
- "What's our regrettable attrition rate? Who left that we wish hadn't?"
- "Are managers our retention asset or our attrition cause?"
- "Can a new hire explain their career path in 12 months?"
- "Where are we paying below P50? Who's a flight risk because of it?"
- "What's the cost of this hire vs. the cost of not hiring?"
## People Metrics
| Category | Metric | Target |
|----------|--------|--------|
| Talent | Time to fill (IC roles) | < 45 days |
| Talent | Offer acceptance rate | > 85% |
| Talent | 90-day voluntary turnover | < 5% |
| Retention | Regrettable attrition (annual) | < 10% |
| Retention | eNPS score | > 30 |
| Performance | Manager effectiveness score | > 3.8/5 |
| Comp | % employees within band | > 90% |
| Comp | Compa-ratio (avg) | 0.95–1.05 |
| Org | Span of control (ICs) | 6–10 |
| Org | Span of control (managers) | 4–7 |
## Red Flags
- Attrition spikes and exit interviews all name the same manager
- Comp bands haven't been refreshed in 18+ months
- No career ladder → top performers leave after 18 months
- Hiring without a written business case or job scorecard
- Performance reviews happen once a year with no mid-year check-in
- Equity refreshes only for executives, not high performers
- Time to fill > 90 days for critical roles
- eNPS below 0 — something is structurally broken
- More than 3 org layers between IC and CEO at < 50 people
## Integration with Other C-Suite Roles
| When... | CHRO works with... | To... |
|---------|-------------------|-------|
| Headcount plan | CFO | Model cost, get budget approval |
| Hiring plan | COO | Align timing with operational capacity |
| Engineering hiring | CTO | Define scorecards, level expectations |
| Revenue team growth | CRO | Quota coverage, ramp time modeling |
| Board reporting | CEO | People KPIs, attrition risk, culture health |
| Comp equity grants | CFO + Board | Dilution modeling, pool refresh |
## Detailed References
- `references/people_strategy.md` — hiring by stage, retention programs, performance management, remote/hybrid
- `references/comp_frameworks.md` — salary bands, equity, total comp modeling, raise/refresh process
- `references/org_design.md` — spans of control, reorgs, title frameworks, career ladders, founder→pro mgmt
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- Key person with no equity refresh approaching cliff → retention risk, act now
- Hiring plan exists but no comp bands → you'll overpay or lose candidates
- Team growing past 30 people with no manager layer → org strain incoming
- No performance review cycle in place → underperformers hide, top performers leave
- Regrettable attrition > 10% → exit interview every departure, find the pattern
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Build a hiring plan" | Headcount plan with roles, timing, cost, and ramp model |
| "Set up comp bands" | Compensation framework with bands, equity, benchmarks |
| "Design our org" | Org chart proposal with spans, layers, and transition plan |
| "We're losing people" | Retention analysis with risk scores and intervention plan |
| "People board section" | Headcount, attrition, hiring velocity, engagement, risks |
## Reasoning Technique: Empathy + Data
Start with the human impact, then validate with metrics. Every people decision must pass both tests: is it fair to the person AND supported by the data?
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:chro-advisor/references/comp_frameworks.md
# Compensation Frameworks Reference
Salary bands, equity design, total comp modeling, comp philosophy, and raise/refresh processes.
---
## Comp Philosophy — The Foundation
Before building bands, define your philosophy. Ambiguity in comp philosophy = pay equity lawsuits and trust erosion.
**The five decisions:**
### 1. What market percentile do you target?
- **P25 (below market):** Only viable with exceptional mission, equity, or growth opportunity. Flight risk is high after 18 months.
- **P50 (market median):** Standard for most Series A–B companies. Competitive without premium.
- **P75 (above market):** Premium talent strategy. Used by high-margin or talent-intensive businesses. Netflix model.
- **P90+:** Top-of-market for specific functions (ML at AI companies, senior engineers at FAANG feeders).
**Common hybrid:** P50 base + above-market equity = total comp at P65–75.
### 2. What's in your total comp package?
Define each component explicitly:
- **Base salary** — cash, market-benchmarked
- **Variable / bonus** — % of base, tied to what criteria
- **Equity** — options vs. RSUs, vesting schedule, refresh cadence
- **Benefits** — health, retirement, PTO policy
- **Learning & development budget**
- **Remote/location allowances**
### 3. Are bands public internally?
Recommended: Yes. Pay transparency reduces equity complaints, builds trust, and forces you to maintain clean bands.
### 4. How often do you refresh bands?
Minimum: annually. High-growth markets: every 6 months (engineering specifically in hot markets).
### 5. How do you handle individual negotiation?
Options:
- **Fixed bands, no negotiation** (Buffer model) — simple, fair, loses some candidates
- **Band range with manager discretion** — most common, requires calibration guardrails
- **Individual negotiation within band** — flexible, creates pay equity drift over time
---
## Salary Bands: Construction
### Step 1: Define levels
Standard IC levels (adapt to company):
| Level | Title example | Scope |
|-------|--------------|-------|
| L1 | Junior / Associate | Execution with guidance |
| L2 | Mid-level | Independent execution |
| L3 | Senior | Leads workstreams, mentors L1-L2 |
| L4 | Staff / Principal | Cross-team technical leadership |
| L5 | Distinguished / Fellow | Company-wide technical direction |
Management track:
| Level | Title | Scope |
|-------|-------|-------|
| M1 | Manager | Team of 4–8 ICs |
| M2 | Senior Manager | Manager of managers or larger team |
| M3 | Director | Function or large org |
| M4 | VP | Business unit, company-wide |
| M5 | SVP / C-Suite | Executive |
### Step 2: Gather market data
**Data sources (by quality):**
1. **Radford / Aon** — Gold standard. Expensive ($10K+/year). Worth it at Series B+.
2. **Levels.fyi** — Excellent for engineering. Free. Self-reported but large sample.
3. **Glassdoor Salary** — Broad coverage. Less precise for startups.
4. **Pave / Carta Total Comp** — VC-backed companies. Good peer benchmarking.
5. **LinkedIn Salary** — Free tier. Reasonable signal for G&A roles.
6. **Offer letter data** — What candidates are bringing from other companies. Real-time signal.
**What to pull:** P25, P50, P75, P90 for each role × level × geography.
### Step 3: Set band structure
**Band width (range within a level):**
- IC bands: 80–120% of midpoint (i.e., ±20% from center)
- Manager bands: 85–115% of midpoint
- Wider bands allow room for differentiation within level; narrower bands reduce pay equity drift
**Band overlap between levels:**
- 10–20% overlap is normal (top of L2 overlaps with bottom of L3)
- > 30% overlap: your levels are too close together
- No overlap: new hires jump too much between levels (compression risk)
**Example engineering band structure (US, Series B company, P50 target):**
| Level | Band Min | Midpoint | Band Max |
|-------|----------|----------|----------|
| L1 Software Engineer | $90K | $105K | $125K |
| L2 Software Engineer | $115K | $135K | $160K |
| L3 Senior SWE | $150K | $175K | $205K |
| L4 Staff SWE | $195K | $225K $260K |
| M1 Eng Manager | $175K | $205K | $235K |
| M2 Sr Eng Manager | $215K | $250K | $285K |
| M3 Director, Eng | $255K | $300K | $345K |
*Adjust by 15–25% for non-SF/NYC markets. Adjust -40% to -60% for European markets.*
### Step 4: Place employees in bands
**Compa-ratio** = Employee salary / Band midpoint
| Compa-ratio | Interpretation |
|------------|---------------|
| < 0.85 | Below range — immediate risk |
| 0.85–0.95 | Developing in role |
| 0.95–1.05 | Fully performing (target zone) |
| 1.05–1.15 | Senior/expert in role |
| > 1.15 | Above range — flag for review |
**Audit report:** Run quarterly. Flag anyone below 0.85 (flight risk) or above 1.15 (overpaid for level, or needs promotion).
---
## Equity Frameworks for Startups
### Option Basics
**ISO vs NSO:**
- ISO (Incentive Stock Options): For employees. Favorable tax treatment if held 1+ year post-exercise.
- NSO (Non-Qualified Stock Options): For advisors, contractors, sometimes employees. Taxed as ordinary income on exercise.
**Strike price:** Set to 409A valuation at grant. Lower is better for employees. Early employees win on strike price.
**Vesting schedule standards:**
- 4-year vest, 1-year cliff: Standard
- 4-year vest, 6-month cliff: Startup market adapting to faster pace
- 1-year cliff means: nothing until 12 months; monthly or quarterly after
**Post-termination exercise window (PTEW):**
- Standard: 90 days. Often too short for employees who can't afford exercise.
- Better: 1–5 years or until IPO. Use as a talent differentiator.
- Companies extending PTEW: Stripe, Airbnb (pre-IPO), Square, most employee-friendly startups.
### Equity Grant Ranges by Stage and Level
*Expressed as % of fully diluted shares at grant. Ranges vary significantly by market, stage, and funding.*
**Seed stage:**
| Role | Equity % |
|------|----------|
| Co-founder | 20–40% |
| First engineering hire | 0.5–1.5% |
| First non-technical exec hire | 0.25–0.75% |
| IC (L2-L3) | 0.1–0.4% |
| IC (L3-L4) | 0.2–0.6% |
**Series A:**
| Role | Equity % |
|------|----------|
| VP / Head of function | 0.3–0.75% |
| Director | 0.1–0.3% |
| Senior IC (L3) | 0.05–0.15% |
| Mid IC (L2) | 0.02–0.08% |
| Junior IC (L1) | 0.01–0.05% |
**Series B:**
| Role | Equity % |
|------|----------|
| VP / Head of function | 0.1–0.3% |
| Director | 0.05–0.15% |
| Senior IC (L3) | 0.02–0.07% |
| Mid IC (L2) | 0.01–0.03% |
*At Series B+, equity is increasingly expressed in dollar value (grant value = X shares × current 409A). Use Carta or Pulley to model dilution.*
### Equity Refresh Program
**Why it matters:** Employees hired at Series A with 4-year vesting will be fully vested by Series B. No unvested equity = no retention hook.
**When to refresh:**
- After every significant funding round
- Annually for high performers (top 20%)
- After promotion (role-commensurate top-up)
- Counter-offer situations (use carefully — signals you underpaid initially)
**Refresh models:**
1. **Anniversary grant:** Annual cliff-free refresh for all employees above a performance threshold
2. **Evergreen model:** Continuous vesting maintained — refresh annually so employee always has 2–3 years remaining
3. **Event-based:** Refresh tied to milestones (promotion, funding, annual review cycle)
**Dilution awareness:** Every refresh dilutes existing shareholders. Model pool usage quarterly. Replenish option pool before it drops below 10–12% of fully diluted shares.
---
## Total Comp Modeling
### Components of Total Comp
```
Total Compensation = Base Salary
+ Annual Bonus (target %)
+ Equity Value (annualized grant / vesting period)
+ Benefits (employer-paid premiums, retirement match)
+ Allowances (home office, internet, L&D, commuter)
```
### Annualizing Equity Value
For comparison to cash compensation:
```
Annual equity value = (Grant shares × Current 409A price) / Vesting years
```
Example: 10,000 options at $2 strike, current 409A = $8, 4-year vest
- Grant value at current 409A = 10,000 × $8 = $80,000
- Annual value = $80,000 / 4 = $20,000/year
- If base is $150K, total comp is ~$170K/year
*Note: For recruiting purposes, you can use last preferred share price (VC price) to show upside — but be transparent about the difference between 409A and preferred.*
### Benefits Valuation
Frequently undervalued in offers. Quantify explicitly:
| Benefit | Typical employer cost |
|---------|----------------------|
| Health insurance (employee) | $4K–8K/year |
| Health insurance (family) | $15K–25K/year |
| 401K match (4% of salary) | $5K–10K/year |
| L&D budget ($2K/year) | $2K/year |
| Home office stipend ($500) | $500/year |
A $140K offer with family health coverage + 4% 401K match is worth $165K+ total.
---
## Raise and Refresh Process
### Annual Compensation Review Cycle
**Recommended cadence:**
- October/November: Market data refresh, band updates
- November/December: Manager merit recommendations
- December/January: Calibration and approvals
- January/February: Effective date for new salaries + equity grants
**Budget allocation:**
- **Merit budget** (performance-based raises): 3–5% of total payroll typically
- **Market adjustment budget** (fixing below-band salaries): Separate from merit. Non-negotiable to avoid attrition.
- **Promotion budget:** Separate. Promotions should not come from merit pool.
### Merit Increase Guidelines
| Performance Rating | Merit Increase Range |
|-------------------|---------------------|
| 5 – Exceptional | 8–15% |
| 4 – Exceeds | 5–8% |
| 3 – Meets | 2–4% |
| 2 – Needs improvement | 0–1% |
| 1 – Underperforming | 0% (PIP active) |
*Adjust based on compa-ratio. A high performer at P90 of their band gets a smaller increase than a high performer at P50.*
### Compa-Ratio Adjustment Matrix
| Performance \ Compa-Ratio | < 0.90 | 0.90–1.00 | 1.00–1.10 | > 1.10 |
|---------------------------|--------|-----------|-----------|--------|
| Exceptional (5) | 12–15% | 8–12% | 5–8% | 3–5% |
| Exceeds (4) | 8–12% | 5–8% | 3–5% | 1–3% |
| Meets (3) | 5–8% | 3–5% | 2–3% | 0–2% |
| Needs impr (2) | 0–2% | 0–1% | 0% | 0% |
### Promotion vs. Merit — Keep These Separate
**Common mistake:** Using merit budget to fund promotions. This forces a choice between rewarding performance and recognizing level change.
**Promotion increase guidelines:**
- One level (e.g., L2 → L3): 10–20% increase, new equity grant
- Two levels (rare): 20–35% increase, new equity grant at new level
- Manager track (IC → M1): 15–25% increase, new equity grant
**Promotion criteria process:**
1. Manager nominates with written business case
2. Calibration committee reviews cross-functionally
3. HR validates against band (no off-band exceptions without CHRO sign-off)
4. Employee informed before annual review — never surprised at review meeting
### Off-Cycle Adjustments
When to do them:
- Counter-offer situations (see below)
- Competitive intelligence reveals underpay for a specific role
- New market data shows a role significantly under-benchmarked
- Internal equity audit reveals unexplained gaps
**Counter-offer policy:**
Three options:
1. **Match** — Risk: signals you underpay; sets precedent
2. **Partial match** — "We can do X, which is the top of your band" — cleaner
3. **Decline** — Accept the attrition, improve the band for the next hire
**Rule:** If you're regularly in counter-offer conversations, your bands are stale. Fix the bands.
---
## Pay Equity Audit
Run annually. Non-negotiable at Series B+.
**What to audit:**
- Pay gap by gender within each level and function
- Pay gap by ethnicity within each level and function
- Compa-ratio distribution across demographics
- Time-to-promotion by demographic group
**Methodology:**
1. Pull all employee data: level, function, salary, tenure, performance ratings, gender, ethnicity
2. Run regression controlling for level, tenure, and performance
3. Unexplained gap after controls = the problem to fix
4. Flag and remediate within the same review cycle
**Legal exposure:** In many jurisdictions, documented pay gaps without remediation plans are litigation risk. The audit creates a record of intent; remediation closes the risk.
**Remediation budget:** Set aside 0.5–1% of payroll annually for equity adjustments. If you're doing it right, this shrinks over time.
FILE:chro-advisor/references/org_design.md
# Org Design Reference
Spans of control, layering decisions, reorgs, title frameworks, career ladders, and the founder→professional management transition.
---
## Core Org Design Principles
1. **Structure follows strategy.** Reorg after strategy shifts, not before.
2. **Optimize for the bottleneck.** Where does work get slow? Design around that.
3. **Minimize coordination cost.** Conway's Law: your org structure becomes your product architecture. Design intentionally.
4. **Bias toward flatness until it breaks.** Adding layers adds cost and slows decisions.
5. **Reorgs have transition costs.** Relationships reset. Count the cost before you restructure.
---
## Spans of Control
Span of control = number of direct reports a manager has.
### Benchmarks
| Role Type | Optimal Span | Min | Max |
|-----------|-------------|-----|-----|
| IC manager (predictable work) | 7–10 | 5 | 12 |
| IC manager (complex/creative work) | 5–7 | 4 | 8 |
| Manager of managers | 4–6 | 3 | 7 |
| VP / Director | 4–7 | 3 | 8 |
| C-Suite | 5–9 | 4 | 10 |
**Too narrow (< 4 ICs):** Over-management, high cost per output, manager becomes a bottleneck
**Too wide (> 12 ICs):** Under-management, degraded 1:1 quality, feedback loops collapse
### Factors that allow wider spans
- Highly autonomous, senior team (L3+ ICs)
- Predictable, well-defined work (support, ops)
- Strong tooling and process (reduces manager overhead)
- Experienced manager
### Factors that require narrower spans
- High-complexity, undefined problems (research, early product)
- Junior or newly promoted team members
- High interdependence between reports (coordination overhead)
- Manager is also an IC contributor (player-coach)
---
## When to Add Management Layers
**The wrong reason to add layers:** "We need to give good people somewhere to grow."
**The right reason:** "This manager has too many direct reports to do the job well."
### Layer triggers by growth stage
**0 → 15 people:** No layers. Everyone reports to founders.
**15 → 30 people:** First managers emerge. Usually technical leads or function leads. Should still be player-coaches.
**30 → 60 people:** Second layer forms. Engineering splits into squads. Sales gets a frontline manager. Each function has a head.
**60 → 150 people:** Director layer becomes necessary in large functions. Engineering VP + Engineering Directors + Team Managers.
**150+ people:** VP layer fully staffed. Senior Director / Director split. Clear IC → M → Senior M → Director → VP paths.
### The Rule of 7
When any manager has 7 or more direct reports and:
- 1:1s are skipped regularly
- Feedback quality drops
- Manager can't answer "how is each person doing?" without checking notes
→ Time to split or hire a manager.
### Management overhead cost
Every manager layer costs 10–15% in decision speed (communication hops).
Every management role without a team = pure overhead.
**Litmus test for each management role:**
- Does this person have at least 4 ICs under them?
- Would removing this role improve decision speed?
- Is this a management job or a "we ran out of IC levels" job?
---
## Functional vs. Product Org Structures
### Functional Structure (by discipline)
```
CEO
├── VP Engineering
│ ├── Backend Team
│ ├── Frontend Team
│ └── DevOps
├── VP Product
│ ├── PM (Feature A)
│ └── PM (Feature B)
└── VP Design
└── UX Designers
```
**Best for:** Early stage, < 100 people, single product
**Advantage:** Deep expertise development, clear career paths per discipline
**Disadvantage:** Cross-functional coordination is heavy; features require synchronization across silos
### Product/Pod Structure (by product area)
```
CEO
├── Product Area A (autonomous team)
│ ├── EM
│ ├── PM
│ └── Designer
├── Product Area B (autonomous team)
│ ├── EM
│ ├── PM
│ └── Designer
└── Platform (shared services)
└── Platform EM + team
```
**Best for:** Multiple products or large user segments, 50+ in product/eng
**Advantage:** Speed and autonomy; less cross-team coordination for most features
**Disadvantage:** Duplication risk; harder to maintain technical coherence; harder career paths
### When to shift from Functional → Product org
- You have 2+ distinct product lines that rarely share features
- Cross-functional feature delivery takes > 3 sprints of coordination overhead
- Teams are > 8 engineers and still waiting on shared resources
### Hybrid / Matrix (avoid unless necessary)
Matrix reporting (e.g., engineer reports to EM + PM) creates accountability confusion. Avoid at < 500 people.
---
## Title Frameworks
### The Problem with Title Inflation
Early startups over-title to compete with cash. "VP of Engineering" with 2 reports. "Head of Marketing" with no team.
**Consequences:**
- Can't add leadership above inflated titles without awkward conversations
- Candidates from mature companies expect scope commensurate with titles
- Internal equity breaks when the same title means different things
### Preventing Title Inflation
**Rule 1:** VP titles require managing managers (not just ICs).
**Rule 2:** Director titles require managing multiple ICs or a large function.
**Rule 3:** No more than one "Head of X" per function.
**Rule 4:** Document scope expectations per title before making offers.
### Engineering Title Ladder (example)
| Title | Level | Scope | Reports |
|-------|-------|-------|---------|
| Software Engineer I | L1 | Executes defined tasks | — |
| Software Engineer II | L2 | Independent delivery | — |
| Senior Software Engineer | L3 | Leads features, mentors | — |
| Staff Software Engineer | L4 | Cross-team technical leadership | — |
| Principal Software Engineer | L5 | Company-wide technical direction | — |
| Distinguished Engineer | L6 | External recognition, defining practice | — |
| Engineering Manager | M1 | Team of 4–8 engineers | 4–8 ICs |
| Senior Engineering Manager | M2 | Larger team or manager of managers | 2–4 managers |
| Director of Engineering | M3 | Functional area | Multiple managers |
| VP of Engineering | M4 | Engineering org | Directors |
| CTO | M5 | Technical organization + strategy | VPs |
**IC vs. Management track:** Explicitly separate. Senior ICs should not need to move to management for career advancement. Staff/Principal/Distinguished track provides this.
### Go-to-Market Title Ladder (example)
| Title | Level | Focus |
|-------|-------|-------|
| SDR / BDR | S1 | Outbound prospecting |
| Account Executive I | S2 | SMB closing |
| Account Executive II | S3 | Mid-market closing |
| Senior Account Executive | S4 | Enterprise closing |
| Principal / Strategic AE | S5 | Named accounts, complex deals |
| Sales Manager | M1 | 6–8 reps |
| Director of Sales | M2 | Multiple teams or segments |
| VP of Sales | M3 | Full sales org |
| CRO | M4 | Revenue org (sales + CS + marketing) |
---
## Career Ladders
A career ladder is a documented set of expectations per level. Not aspirational — behavioral. "What does a P3 engineer do that a P2 doesn't?"
### Why career ladders matter for HR
1. **Retention:** Employees can see where they're going
2. **Consistency:** Managers use the same criteria for promotions
3. **Compensation:** Bands anchor to levels; levels require definitions
4. **Equity:** Removes "who's the manager's favorite" from promotion decisions
### Career Ladder Structure
For each level, define 4 dimensions:
**1. Scope** — How big is the problem space? Team / cross-team / org-wide / company-wide?
**2. Impact** — How does work connect to outcomes? (Task → Feature → Product → Business)
**3. Craft** — Technical/functional skill expectations
**4. Influence** — How does this person improve others? (Self → peers → team → org)
**Example: Senior Software Engineer (L3) vs. Staff Software Engineer (L4)**
| Dimension | L3 (Senior SWE) | L4 (Staff SWE) |
|-----------|----------------|----------------|
| Scope | Owns features or services | Owns technical domains across teams |
| Impact | Ships features that improve user outcomes | Shapes technical direction for a product area |
| Craft | Writes high-quality code, good design skills | Sets coding standards, contributes to architecture |
| Influence | Mentors L1–L2, code reviews | Mentors L3+, identifies org-wide technical gaps |
### How to build a career ladder from scratch
1. **Interview your best performers** — "What do you do that your junior peers don't?" Collect behaviors, not aspirations.
2. **Draft 3 levels** — Don't start with 6. Start with junior, mid, senior. Add staff/principal only when you have enough people to warrant it.
3. **Manager calibration** — Every manager rates 5 current employees against the draft. Gaps surface immediately.
4. **Publish and iterate** — Don't wait for perfection. A 70% ladder shipped is better than a 100% ladder in a drawer.
---
## Reorg Playbook
### When reorgs are necessary
- Strategy pivot requires different team structure (e.g., single product → multi-product)
- Acquisition or team merger
- Function is genuinely too slow due to coordination overhead
- Leadership departure creates structural opportunity
### When reorgs are a mistake
- "We need to shake things up" (disruption for its own sake)
- Avoiding a specific personnel decision (use the right tool)
- Solving a cultural problem with a structural change
- Reacting to one team's complaint without systemic evidence
### Reorg Process (4–8 weeks)
**Week 1–2: Diagnose**
- Map current org: every role, reporting line, team output
- Identify where work is slow, duplicated, or falling through cracks
- Interview 5–10 people across teams: "What takes longer than it should? What decisions are hard to make?"
**Week 3–4: Design options**
- Draft 2–3 structural alternatives
- For each: estimated coordination costs, manager span impact, open roles created
- Validate with CEO + 1–2 trusted operators. Don't crowdsource the design.
**Week 5–6: Decide and prepare**
- Select option; finalize all reporting changes
- Prepare communications for every affected person (individual conversations before all-hands)
- Write the "why" — employees need to understand the business reason, not just the result
**Week 7–8: Communicate and implement**
- Individual conversations with all manager+ changes (first)
- Team-level conversations with managers (second)
- All-hands with full context (third)
- Updated org chart published within 24 hours of announcement
### Communication sequence (non-negotiable)
1. Affected individuals first (private, before anything else)
2. Affected managers second (to prepare for team conversations)
3. Full team/company third (all-hands or company note)
4. External (clients, board) only if materially impacted
**Never:** Email blast first. No individual conversations. Discovered on the org chart.
---
## Founder → Professional Management Transition
The most common scaling failure point in startups.
### Stage 1: Founder-Led (0–30 people)
Founders make all decisions, know everyone personally, set culture through behavior. Works because trust and context are built directly.
**What breaks:**
- Decisions bottleneck at founders
- New hires don't get enough context (founders can't be everywhere)
- Culture transmitted through osmosis, not documentation
### Stage 2: First Managers (30–80 people)
Founders can no longer manage all ICs. First manager layer typically = promoted high performers.
**The "brilliant IC → struggling manager" trap:**
- Individual contributor skills ≠ management skills
- Promoted ICs often continue doing IC work while ignoring management work
- No one holds them accountable to management output (1:1 quality, team health, performance feedback)
**What to do:**
- Explicit manager training before promotion (not after)
- Management KPIs separate from IC KPIs
- Peer community for new managers (monthly cohort session)
- HR check-ins on manager health at 30/60/90 days
### Stage 3: Professional Management (80–200 people)
External hires at Director/VP level bring professional management skills but lack company context.
**Common failure modes:**
- Hired "too senior" — VP who's used to 200-person teams in a 50-person function
- Culture clash — Big-company manager who adds process that kills startup speed
- Authority vacuum — External VP doesn't earn trust; team ignores them; founder continues to bypass hierarchy
**Mitigation:**
- Hiring bar: Has this person scaled from this stage to 2x this stage before? Not managed a team at 2x — built a team to 2x.
- Explicit onboarding on "how we make decisions here"
- 90-day milestones focused on relationship-building before any structural changes
- Founders explicitly hand off ownership and reinforce new manager's authority publicly
### Stage 4: Founder Transition from Operator to Executive
The hardest personal transition. Founder moves from doing to enabling.
**Signs you haven't made the transition:**
- You're still in every technical decision
- Teams come to you instead of their manager for approvals
- You know more about the team's work than the manager does
- Managers feel they need to check in before acting
**What the transition requires:**
- Explicit authority delegation in writing (not just verbal)
- Willingness to let managers make decisions you'd make differently
- Redirecting team members to their manager consistently
- Measuring managers on outcomes, not just process adherence
- Letting managers hire and fire without founder override (except final call on VPs)
FILE:chro-advisor/references/people_strategy.md
# People Strategy Reference
Hiring, retention, performance, and remote/hybrid frameworks for each growth stage.
---
## Hiring Strategy by Growth Stage
### Pre-Seed / Seed (1–15 people)
**Who you're hiring:** Generalists who can do multiple jobs. Specialists are a luxury you can't afford unless the specialty is your core product.
**The test:** Could this person be the 5th employee at a startup and thrive? If they need a defined role, clear process, and a manager — not yet.
**Sourcing at this stage:**
- Founder networks first (highest signal, lowest cost)
- Angel List / Wellfound — self-selected for startup risk tolerance
- Referrals from existing employees (offer a referral bonus from day 1)
- GitHub / Dribbble / published work for technical roles
- Avoid: Big job boards, recruiters (unless technical retained search for C-suite)
**Interview process (keep it lean):**
1. 30-min intro call (culture/motivation fit, comp alignment)
2. Take-home or live work sample (2–4 hours max, paid for senior roles)
3. 60-min deep-dive with founders
4. Reference checks (3 calls, not emails — you want the real story)
**Offer timeline:** Decision within 48 hours. Top candidates have multiple offers.
**What to get right:**
- Written job scorecard (outcomes expected in 30/60/90 days) — not a job description
- Equity range disclosed in first conversation
- No exploding offers. Pressure tactics lose good people.
---
### Series A (15–50 people)
**The hiring shift:** You need some specialists now. First management layer emerges. First "culture carries" — people who reinforce what you want to become.
**Critical hires at this stage (in priority order):**
1. VP/Head of Engineering (if founder isn't technical)
2. Head of Product
3. First dedicated recruiter (when you're hiring > 10/year)
4. First Finance/Operations hire
5. Head of Sales (when product-market fit is real)
**Building the recruiting function:**
- First recruiter should be a generalist with hustle, not a specialist
- Set up an ATS (Ashby, Greenhouse, or Lever) before you need it — not after
- Create interview scorecards for every role
- Track: time to fill, offer acceptance rate, source quality
**Common mistakes at Series A:**
- Promoting top ICs to management without management training
- Hiring "brand name" executives who've never operated lean
- Over-indexing on experience, under-indexing on trajectory
- No onboarding process → 90-day regrettable turnover
**Job scorecards (required for every role):**
```
Role: [Title]
Reports to: [Manager]
Start date: [Target]
Why this role now: [Business case in 1-2 sentences]
Outcomes (90 days):
- [Concrete deliverable 1]
- [Concrete deliverable 2]
- [Concrete deliverable 3]
Outcomes (12 months):
- [Strategic impact 1]
- [Strategic impact 2]
Competencies (top 3 only):
- [What, why it matters for THIS role]
- [What, why it matters for THIS role]
- [What, why it matters for THIS role]
Comp range: [Base] + [Equity] + [Benefits summary]
```
---
### Series B (50–150 people)
**The scaling inflection point.** Tribal knowledge breaks. Process matters now. Culture requires deliberate investment.
**What changes:**
- Recruiters become specialists (technical, GTM, exec)
- Manager training becomes non-negotiable
- Performance management needs structure (not just "we'll know it when we see it")
- Onboarding needs to scale without founders in every session
- Comp bands become essential — people are comparing notes
**Hiring velocity benchmarks (Series B):**
| Function | Avg time to fill | Avg interviews | Benchmark offer acceptance |
|----------|-----------------|----------------|---------------------------|
| Engineering IC | 35–45 days | 4–5 rounds | 80–85% |
| Engineering Manager | 45–60 days | 5–6 rounds | 75–80% |
| Sales IC | 25–35 days | 3–4 rounds | 85–90% |
| Sales Manager | 40–55 days | 4–5 rounds | 80–85% |
| G&A (Finance, HR, Ops) | 30–45 days | 3–4 rounds | 85–90% |
**Internal mobility:** By 50 people, start tracking internal promotion rates. Target: 20–30% of manager+ roles filled internally. If it's < 10%, your career development is failing.
---
### Series C+ (150+ people)
**Professional management era.** Founders can't know everyone. Systems and culture carry what personal relationships used to.
**HR function maturity required:**
- Dedicated HRBPs per business unit (1:75–100 employees)
- L&D budget (1–2% of salary budget minimum)
- Succession planning for all VP+ roles
- Structured calibration process for performance reviews
- Total rewards strategy reviewed annually with board
---
## Retention Programs That Actually Work
### What drives retention (in order of impact)
1. **Manager quality** — Gallup: 70% of team engagement variance is explained by the manager. Fix managers first.
2. **Growth trajectory** — People leave when they can't see their next role. Career ladders are retention tools.
3. **Compensation competitiveness** — Being at P25 on salary is a slow leak. Audit annually.
4. **Mission/product belief** — Especially for senior ICs. They want to work on something that matters.
5. **Team quality** — "I stay because of the people I work with." True at every level.
6. **Flexibility** — Location, hours, autonomy. Low cost, high impact.
### What doesn't work (but companies do anyway)
- Pizza parties and ping pong tables
- "Perks" that substitute for salary
- Annual reviews with no action on feedback
- Forced fun events
- Vague "culture improvement" initiatives without specific behavior changes
### The 30-60-90 Onboarding Framework
Structured onboarding cuts 90-day turnover by 50%+.
**Days 1–30: Learn**
- Complete admin setup (day 1, before lunch)
- Meet all key stakeholders (scheduled by their manager, not on the new hire)
- Understand: business model, current priorities, team processes, how success is measured
- No deliverables expected. Learning is the job.
- Weekly 1:1 with manager: "What's confusing? What do you need?"
**Days 31–60: Contribute**
- First real project (scoped to be completable)
- Present findings or work to the team
- Identify one process that could be improved (observation only — don't fix yet)
- 30-day check-in: formal feedback from manager
**Days 61–90: Lead**
- Own a deliverable end-to-end
- Offer one specific improvement recommendation with data
- 90-day review: mutual assessment — manager on new hire, new hire on onboarding
- Set 6-month goals
### Stay Interviews (underused, high ROI)
Run with every employee once per year. Not their manager — HR or skip-level.
**Questions that surface real risk:**
- "What's keeping you here?"
- "What would make you consider leaving?"
- "What's one thing your manager could do differently?"
- "Is your role what you expected when you joined?"
- "What career path do you want? Are we helping you get there?"
- "Are you fairly compensated? Do you know how you'd get a raise?"
**Act on answers within 30 days or don't ask.** Unanswered feedback is worse than no feedback.
### Exit Interviews — What to Actually Learn
Skip the happiness survey. Ask these:
- "When did you first think about leaving?"
- "Was there a specific event that triggered your decision?"
- "What could we have done to retain you?"
- "Where are you going and why?" (What does the other offer have that we don't?)
- "Would you recommend us as an employer? Why or why not?"
Track exit themes by manager. If one manager's exits cite "micromanagement" three times — that's data.
---
## Performance Management
### The System That Works
**Continuous > annual.** Annual reviews with no mid-year touchpoints are theater.
**Structure:**
- **Weekly 1:1s** (30 min): blockers, priorities, relationship
- **Monthly check-ins** (1 hr): progress against goals, feedback exchange
- **Quarterly reviews** (formal): written self-assessment + manager assessment + goal revision
- **Annual calibration** (rating + comp): cross-manager calibration session, then individual conversations
### Calibration Sessions
**Purpose:** Prevent manager bias. Ensure "exceeds expectations" means the same thing across teams.
**Process:**
1. Managers submit preliminary ratings independently
2. HR facilitates 2-hr calibration with all managers in a function
3. Managers must justify outliers (top and bottom)
4. Ratings adjusted for consistency
5. Managers deliver final ratings with rationale
**Distribution guidance (enforce with calibration):**
- Exceptional (5): < 10% — if everyone's exceptional, no one is
- Exceeds (4): 20–25%
- Meets (3): 55–65%
- Needs improvement (2): 8–12%
- Underperforming (1): 2–5%
### Managing Underperformers
**The most avoided management task. And the most damaging when avoided.**
High performers notice when underperformers are tolerated. They leave.
**The 4-step framework:**
**Step 1: Diagnose before acting** (Week 1–2)
- Is this a skill gap (can't do it) or a will gap (won't do it)?
- Skill gap → training, clearer expectations, different role
- Will gap → direct feedback, clear consequences, then PIP
**Step 2: Direct feedback conversation** (Week 2–3)
- Specific: "Your last 3 sprint deliveries were 40% incomplete"
- Not: "You're not meeting expectations"
- Document. Send written summary after every feedback conversation.
**Step 3: Performance Improvement Plan (PIP)**
Required when: two rounds of direct feedback haven't produced change.
PIP structure:
```
Name: [Employee]
Manager: [Name]
Date: [Start]
Review date: [30/60 days out]
Current performance issues:
- [Specific, observable behavior with examples and dates]
- [Metric not met: target X, actual Y for Z weeks]
Required improvements:
- [Specific, measurable outcome 1] by [date]
- [Specific, measurable outcome 2] by [date]
Support provided:
- [Training, coaching, additional resources]
Consequences if not met: [Role change / separation]
Check-in schedule: [Weekly with manager + HR]
```
**Step 4: Exit or role change**
- If PIP milestones not met: proceed to separation
- Don't extend PIPs indefinitely — it's unfair to the employee and the team
- Offer a graceful exit where possible: "This role isn't the right fit. Here's a package and a reference."
**What not to do:**
- "Quiet manage out" without clear feedback (legally risky, unfair)
- PIP as a formality before termination (if you know you're firing them, just do it)
- Tolerating underperformance "because we're understaffed" (it makes understaffing worse)
---
## Remote / Hybrid Strategy
### The question isn't "remote or not" — it's "what kind of collaboration does our work require?"
**Work type taxonomy:**
| Work type | Remote-compatible? | Hybrid compatible? |
|-----------|-------------------|-------------------|
| Deep individual work (coding, writing, analysis) | Yes | Yes |
| Async collaboration (code review, doc review) | Yes | Yes |
| Synchronous problem-solving (debugging, design) | Yes (video) | Yes |
| Relationship-building (onboarding, new team) | Harder | Yes |
| Executive alignment, strategy | Harder | Yes — quarterly in-person |
| Sales (enterprise, relationship-based) | No | Depends on market |
### Making Hybrid Work (Not Just a Policy)
**The failure mode:** "Hybrid" = go to office on Tuesday/Thursday, but no one coordinates, all meetings are still Zoom anyway.
**What actually works:**
1. **Anchor days with purpose** — Office days should have things that require the office: workshops, team rituals, whiteboarding sessions. Not just "presence."
2. **Async-first culture, not async-only** — Document decisions. Write things down. Use Loom for walkthroughs. Reduce "quick sync" meetings.
3. **Equal experience for remote participants** — If some are in the room and some are on video, the remote folks are second-class. Either everyone's remote or set up rooms properly.
4. **Manager standards for remote teams:**
- 1:1s are non-negotiable (video, not async)
- Over-communicate on priorities (people can't absorb hallway context)
- Write down decisions (remote employees miss casual office decisions)
- Recognize work publicly (Slack shoutouts, all-hands wins)
### Remote Compensation Philosophy (pick one, be explicit)
**Option A: Location-based pay**
Pay based on where the employee lives. Lower cost in lower-cost markets. Harder to hire in high-cost cities.
**Option B: Role-based (location-neutral)**
One band for each role regardless of location. Simpler, more equitable. Higher overall payroll cost.
**Option C: Zone-based**
Define 2–3 geographic zones (e.g., Tier 1 cities, Tier 2 cities, international). Set bands per zone. Common at mid-stage startups.
**The wrong answer:** No stated policy, and every offer is negotiated individually. Creates pay equity problems fast.
FILE:chro-advisor/scripts/comp_benchmarker.py
#!/usr/bin/env python3
"""
Compensation Benchmarker
========================
Salary benchmarking and total comp modeling for startup teams.
Analyzes pay equity, compa-ratios, and total comp vs. market.
Usage:
python comp_benchmarker.py # Run with built-in sample data
python comp_benchmarker.py --config roster.json # Load from JSON
python comp_benchmarker.py --help
Output: Band compliance report, compa-ratio distribution, pay equity flags,
equity value analysis, and total comp vs. market.
"""
import argparse
import json
import csv
import io
import sys
from dataclasses import dataclass, field, asdict
from typing import Optional
from datetime import date
import math
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
@dataclass
class BandDefinition:
"""Salary band for a role level."""
level: str # L1, L2, L3, L4, M1, M2, M3, VP
function: str # Engineering, Sales, Product, G&A, Marketing, CS
band_min: int # Annual USD
band_mid: int # P50 anchor
band_max: int # Band ceiling
market_p25: int # Market 25th percentile
market_p50: int # Market median (should align with band_mid for P50 strategy)
market_p75: int # Market 75th percentile
location_zone: str # Tier1 (SF/NYC), Tier2 (Austin/Denver), Tier3 (Remote/other), EU
@dataclass
class Employee:
"""One employee record."""
id: str
name: str
role: str
level: str
function: str
location_zone: str
base_salary: int
bonus_target_pct: float # % of base
equity_shares: int # Total unvested options/RSUs
equity_strike: float # Strike price (0 for RSUs)
equity_current_409a: float # Current 409A share price
equity_vest_years_remaining: float # How many years of vesting remain
benefits_annual: int # Employer-paid benefits cost
gender: str # M/F/NB/Undisclosed (for equity audit)
ethnicity: str # For equity audit — can be "Undisclosed"
tenure_years: float
performance_rating: int # 1–5
last_raise_months_ago: int
last_equity_refresh_months_ago: Optional[int] = None
@dataclass
class CompRoster:
company: str
as_of_date: str # ISO date
funding_stage: str # Seed, Series A, Series B, etc.
comp_philosophy_target: str # P50, P65, P75 — your target percentile
preferred_stock_price: float # Last round price (for offer modeling)
employees: list[Employee] = field(default_factory=list)
bands: list[BandDefinition] = field(default_factory=list)
# ---------------------------------------------------------------------------
# Band lookup
# ---------------------------------------------------------------------------
def find_band(roster: CompRoster, level: str, function: str, zone: str) -> Optional[BandDefinition]:
"""Find best-matching band. Falls back to any matching level+function if zone not found."""
matches = [b for b in roster.bands if b.level == level and b.function == function and b.location_zone == zone]
if matches:
return matches[0]
# Fallback: same level+function, any zone
matches = [b for b in roster.bands if b.level == level and b.function == function]
if matches:
return matches[0]
# Fallback: same level, any function
matches = [b for b in roster.bands if b.level == level]
if matches:
return matches[0]
return None
# ---------------------------------------------------------------------------
# Compensation analysis
# ---------------------------------------------------------------------------
def compa_ratio(salary: int, band_mid: int) -> float:
return salary / band_mid if band_mid > 0 else 0.0
def band_position(salary: int, band_min: int, band_max: int) -> float:
"""Position in band: 0.0 = at min, 1.0 = at max."""
if band_max == band_min:
return 0.5
return (salary - band_min) / (band_max - band_min)
def annualized_equity_value(emp: Employee) -> int:
"""Current 409A value of unvested equity, annualized."""
if emp.equity_vest_years_remaining <= 0:
return 0
if emp.equity_current_409a > emp.equity_strike:
intrinsic = (emp.equity_current_409a - emp.equity_strike) * emp.equity_shares
else:
# Options underwater — still show at current FMV for RSUs or future value for options
intrinsic = emp.equity_current_409a * emp.equity_shares if emp.equity_strike == 0 else 0
return int(intrinsic / emp.equity_vest_years_remaining)
def total_comp(emp: Employee) -> int:
bonus = int(emp.base_salary * emp.bonus_target_pct)
equity = annualized_equity_value(emp)
return emp.base_salary + bonus + equity + emp.benefits_annual
def analyze_employee(emp: Employee, roster: CompRoster) -> dict:
band = find_band(roster, emp.level, emp.function, emp.location_zone)
result = {
"id": emp.id,
"name": emp.name,
"role": emp.role,
"level": emp.level,
"function": emp.function,
"zone": emp.location_zone,
"base": emp.base_salary,
"bonus_target": int(emp.base_salary * emp.bonus_target_pct),
"equity_annual": annualized_equity_value(emp),
"benefits": emp.benefits_annual,
"total_comp": total_comp(emp),
"performance": emp.performance_rating,
"tenure_years": emp.tenure_years,
"last_raise_months": emp.last_raise_months_ago,
"band": band,
"compa_ratio": None,
"band_position": None,
"vs_market_p50": None,
"flags": [],
}
if band:
cr = compa_ratio(emp.base_salary, band.band_mid)
bp = band_position(emp.base_salary, band.band_min, band.band_max)
result["compa_ratio"] = round(cr, 3)
result["band_position"] = round(bp, 3)
result["vs_market_p50"] = round((emp.base_salary - band.market_p50) / band.market_p50 * 100, 1)
# Flags
if emp.base_salary < band.band_min:
result["flags"].append(("CRITICAL", "Base below band minimum — immediate attrition risk"))
elif cr < 0.88:
result["flags"].append(("HIGH", f"Compa-ratio {cr:.2f} — significantly below midpoint"))
elif cr < 0.93:
result["flags"].append(("MEDIUM", f"Compa-ratio {cr:.2f} — below target zone (0.95–1.05)"))
if emp.base_salary > band.band_max:
result["flags"].append(("HIGH", "Base above band maximum — review for promotion or band update"))
if emp.performance_rating >= 4 and cr < 0.95:
result["flags"].append(("HIGH", f"High performer (rating {emp.performance_rating}) underpaid — flight risk"))
if emp.last_raise_months_ago > 18:
result["flags"].append(("MEDIUM", f"No raise in {emp.last_raise_months_ago} months — review due"))
if emp.equity_vest_years_remaining < 1.0 and (emp.last_equity_refresh_months_ago is None or emp.last_equity_refresh_months_ago > 24):
result["flags"].append(("HIGH", "Equity nearly fully vested with no refresh — retention hook gone"))
else:
result["flags"].append(("INFO", "No band found for this level/function/zone"))
return result
# ---------------------------------------------------------------------------
# Aggregate analysis
# ---------------------------------------------------------------------------
def pay_equity_audit(analyses: list[dict], employees: list[Employee]) -> dict:
"""Simple pay equity analysis by gender and ethnicity."""
emp_by_id = {e.id: e for e in employees}
def group_stats(group_key_fn):
groups: dict[str, list[float]] = {}
for a in analyses:
if a["compa_ratio"] is None:
continue
emp = emp_by_id.get(a["id"])
if not emp:
continue
key = group_key_fn(emp)
if key not in groups:
groups[key] = []
groups[key].append(a["compa_ratio"])
return {k: {"n": len(v), "avg_cr": round(sum(v)/len(v), 3), "min_cr": round(min(v), 3), "max_cr": round(max(v), 3)}
for k, v in groups.items() if v}
gender_stats = group_stats(lambda e: e.gender)
ethnicity_stats = group_stats(lambda e: e.ethnicity)
# Compute gap vs. the largest group
def compute_gap(stats: dict) -> dict[str, float]:
if not stats:
return {}
largest = max(stats.items(), key=lambda x: x[1]["n"])
ref_cr = largest[1]["avg_cr"]
return {k: round((v["avg_cr"] - ref_cr) / ref_cr * 100, 1) for k, v in stats.items()}
gender_gaps = compute_gap(gender_stats)
ethnicity_gaps = compute_gap(ethnicity_stats)
return {
"gender": gender_stats,
"gender_gaps_pct": gender_gaps,
"ethnicity": ethnicity_stats,
"ethnicity_gaps_pct": ethnicity_gaps,
}
def compa_ratio_distribution(analyses: list[dict]) -> dict:
crs = [a["compa_ratio"] for a in analyses if a["compa_ratio"] is not None]
if not crs:
return {}
buckets = {
"< 0.85 (below band)": 0,
"0.85–0.94 (developing)": 0,
"0.95–1.05 (target zone)": 0,
"1.06–1.15 (senior in role)": 0,
"> 1.15 (above band)": 0,
}
for cr in crs:
if cr < 0.85:
buckets["< 0.85 (below band)"] += 1
elif cr < 0.95:
buckets["0.85–0.94 (developing)"] += 1
elif cr <= 1.05:
buckets["0.95–1.05 (target zone)"] += 1
elif cr <= 1.15:
buckets["1.06–1.15 (senior in role)"] += 1
else:
buckets["> 1.15 (above band)"] += 1
avg = sum(crs) / len(crs)
return {"distribution": buckets, "avg_compa_ratio": round(avg, 3), "n": len(crs)}
# ---------------------------------------------------------------------------
# Report output
# ---------------------------------------------------------------------------
def fmt(n) -> str:
return f",.0f"
def bar(value: float, width: int = 20) -> str:
filled = min(width, max(0, int(value * width)))
return "█" * filled + "░" * (width - filled)
def print_report(roster: CompRoster):
WIDTH = 76
SEP = "=" * WIDTH
sep = "-" * WIDTH
analyses = [analyze_employee(e, roster) for e in roster.employees]
cr_dist = compa_ratio_distribution(analyses)
equity_audit = pay_equity_audit(analyses, roster.employees)
print(SEP)
print(f" COMPENSATION BENCHMARKING REPORT — {roster.company}")
print(f" As of: {roster.as_of_date} | Stage: {roster.funding_stage} | Target: {roster.comp_philosophy_target}")
print(SEP)
# Summary stats
total_emps = len(roster.employees)
flagged = sum(1 for a in analyses if any(s in ["CRITICAL", "HIGH"] for s, _ in a["flags"]))
total_payroll = sum(e.base_salary for e in roster.employees)
avg_total_comp = sum(a["total_comp"] for a in analyses) // total_emps if total_emps else 0
print(f"\n[ SUMMARY ]")
print(sep)
print(f" Employees analyzed: {total_emps}")
print(f" Flagged (critical/high): {flagged}")
print(f" Total base payroll: {fmt(total_payroll)}/year")
print(f" Avg total comp: {fmt(avg_total_comp)}/year")
if cr_dist:
print(f" Avg compa-ratio: {cr_dist['avg_compa_ratio']:.3f}")
# Compa-ratio distribution
if cr_dist:
print(f"\n[ COMPA-RATIO DISTRIBUTION ]")
print(sep)
total_n = cr_dist["n"]
for label, count in cr_dist["distribution"].items():
pct = count / total_n if total_n else 0
bar_str = bar(pct, 25)
print(f" {label:<30} {bar_str} {count:3d} ({pct*100:4.0f}%)")
# Pay equity audit
print(f"\n[ PAY EQUITY AUDIT ]")
print(sep)
print(f" By Gender:")
for group, stats in equity_audit["gender"].items():
gap = equity_audit["gender_gaps_pct"].get(group, 0.0)
gap_str = f" gap: {gap:+.1f}%" if gap != 0 else " (reference group)"
flag = " ⚠" if abs(gap) > 5 else ""
print(f" {group:<15} n={stats['n']} avg_CR={stats['avg_cr']:.3f}{gap_str}{flag}")
print(f"\n By Ethnicity:")
for group, stats in equity_audit["ethnicity"].items():
gap = equity_audit["ethnicity_gaps_pct"].get(group, 0.0)
gap_str = f" gap: {gap:+.1f}%" if gap != 0 else " (reference group)"
flag = " ⚠" if abs(gap) > 5 else ""
print(f" {group:<20} n={stats['n']} avg_CR={stats['avg_cr']:.3f}{gap_str}{flag}")
print(f"\n ⚠ = gap > 5%. Investigate with regression controlling for level, tenure, and performance.")
# Employee detail with flags
print(f"\n[ EMPLOYEE DETAIL ]")
print(sep)
# Group by function
functions = sorted(set(e.function for e in roster.employees))
for fn in functions:
fn_analyses = [a for a in analyses if a["function"] == fn]
if not fn_analyses:
continue
print(f"\n ── {fn} ──")
print(f" {'Name':<22} {'Role':<28} {'Lvl':<5} {'Base':>10} {'TotalComp':>11} {'CR':>6} {'Perf':>5} Flags")
print(f" {'-'*22} {'-'*28} {'-'*5} {'-'*10} {'-'*11} {'-'*6} {'-'*5} {'-'*20}")
for a in sorted(fn_analyses, key=lambda x: -x["base"]):
cr_str = f"{a['compa_ratio']:.2f}" if a["compa_ratio"] else "N/A"
flag_summary = ", ".join(s for s, _ in a["flags"] if s in ("CRITICAL", "HIGH", "MEDIUM"))
flag_str = flag_summary if flag_summary else "OK"
print(f" {a['name']:<22} {a['role']:<28} {a['level']:<5} "
f"{fmt(a['base']):>10} {fmt(a['total_comp']):>11} {cr_str:>6} {a['performance']:>5} {flag_str}")
# Print flag detail for critical/high
for severity, msg in a["flags"]:
if severity in ("CRITICAL", "HIGH"):
print(f" {'':>22} ↳ [{severity}] {msg}")
# Action items
critical = [(a["name"], msg) for a in analyses for sev, msg in a["flags"] if sev == "CRITICAL"]
high = [(a["name"], msg) for a in analyses for sev, msg in a["flags"] if sev == "HIGH"]
medium = [(a["name"], msg) for a in analyses for sev, msg in a["flags"] if sev == "MEDIUM"]
print(f"\n[ ACTION ITEMS ]")
print(sep)
if critical:
print(f"\n CRITICAL — Address this review cycle:")
for name, msg in critical:
print(f" • {name}: {msg}")
if high:
print(f"\n HIGH — Address within 30 days:")
for name, msg in high[:10]:
print(f" • {name}: {msg}")
if len(high) > 10:
print(f" ... and {len(high)-10} more")
if medium:
print(f"\n MEDIUM — Address in next comp cycle:")
for name, msg in medium[:8]:
print(f" • {name}: {msg}")
if len(medium) > 8:
print(f" ... and {len(medium)-8} more")
if not critical and not high and not medium:
print(f"\n No critical or high-severity issues. Compensation appears well-managed.")
# Remediation cost estimate
below_min = [a for a in analyses if a["band"] and a["base"] < a["band"].band_min]
below_mid = [a for a in analyses if a["compa_ratio"] and a["compa_ratio"] < 0.90]
if below_min or below_mid:
print(f"\n[ REMEDIATION COST ESTIMATE ]")
print(sep)
if below_min:
cost_to_min = sum(a["band"].band_min - a["base"] for a in below_min)
print(f" Cost to bring below-minimum to band min: {fmt(cost_to_min)}/year ({len(below_min)} employees)")
if below_mid:
cost_to_90 = sum(int(a["band"].band_mid * 0.90) - a["base"] for a in below_mid if a["base"] < int(a["band"].band_mid * 0.90))
cost_to_90 = max(0, cost_to_90)
print(f" Cost to bring CR < 0.90 to CR = 0.90: {fmt(cost_to_90)}/year ({len(below_mid)} employees)")
total_payroll_impact = sum(e.base_salary for e in roster.employees)
total_remediation = (below_min and cost_to_min or 0)
print(f"\n Total payroll before remediation: {fmt(total_payroll_impact)}/year")
print(f" Remediation as % of payroll: {total_remediation/total_payroll_impact*100:.1f}%")
print(f"\n{SEP}\n")
def export_csv(roster: CompRoster) -> str:
analyses = [analyze_employee(e, roster) for e in roster.employees]
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["ID", "Name", "Role", "Level", "Function", "Zone",
"Base", "Bonus Target", "Equity Annual", "Benefits", "Total Comp",
"Compa Ratio", "Band Position", "vs Market P50 %",
"Performance", "Tenure Years", "Last Raise (mo)",
"Gender", "Ethnicity", "Critical Flags", "High Flags"])
for a, e in zip(analyses, roster.employees):
critical_flags = "; ".join(msg for sev, msg in a["flags"] if sev == "CRITICAL")
high_flags = "; ".join(msg for sev, msg in a["flags"] if sev == "HIGH")
writer.writerow([a["id"], a["name"], a["role"], a["level"], a["function"], a["zone"],
a["base"], a["bonus_target"], a["equity_annual"], a["benefits"], a["total_comp"],
a["compa_ratio"], a["band_position"], a["vs_market_p50"],
a["performance"], a["tenure_years"], a["last_raise_months"],
e.gender, e.ethnicity, critical_flags, high_flags])
return output.getvalue()
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
def build_sample_roster() -> CompRoster:
roster = CompRoster(
company="AcmeTech (Series A)",
as_of_date=date.today().isoformat(),
funding_stage="Series A",
comp_philosophy_target="P50",
preferred_stock_price=8.50,
)
# Bands (Engineering, P50 target, Tier1 = SF/NYC)
roster.bands = [
BandDefinition("L2", "Engineering", 115_000, 132_000, 155_000, 110_000, 132_000, 155_000, "Tier1"),
BandDefinition("L3", "Engineering", 148_000, 170_000, 198_000, 145_000, 170_000, 198_000, "Tier1"),
BandDefinition("L4", "Engineering", 185_000, 215_000, 248_000, 182_000, 215_000, 250_000, "Tier1"),
BandDefinition("M1", "Engineering", 170_000, 195_000, 225_000, 168_000, 195_000, 225_000, "Tier1"),
BandDefinition("L2", "Engineering", 95_000, 108_000, 125_000, 92_000, 108_000, 126_000, "Tier2"),
BandDefinition("L3", "Engineering", 122_000, 140_000, 162_000, 120_000, 140_000, 162_000, "Tier2"),
BandDefinition("L2", "Sales", 80_000, 92_000, 108_000, 78_000, 92_000, 108_000, "Tier1"),
BandDefinition("L3", "Sales", 95_000, 110_000, 128_000, 93_000, 110_000, 128_000, "Tier1"),
BandDefinition("M1", "Sales", 130_000, 150_000, 172_000, 128_000, 150_000, 172_000, "Tier1"),
BandDefinition("L2", "Product", 125_000, 145_000, 168_000, 123_000, 145_000, 168_000, "Tier1"),
BandDefinition("L3", "Product", 155_000, 178_000, 205_000, 153_000, 178_000, 205_000, "Tier1"),
BandDefinition("L2", "G&A", 85_000, 98_000, 115_000, 83_000, 98_000, 115_000, "Tier1"),
BandDefinition("L3", "G&A", 110_000, 128_000, 148_000, 108_000, 128_000, 148_000, "Tier1"),
]
roster.employees = [
# Engineering — mix of scenarios
Employee("E001", "Aarav Shah", "Senior SWE (Backend)", "L3", "Engineering", "Tier1",
base_salary=168_000, bonus_target_pct=0.0, equity_shares=40_000,
equity_strike=1.50, equity_current_409a=6.80, equity_vest_years_remaining=2.5,
benefits_annual=18_000, gender="M", ethnicity="Asian",
tenure_years=2.5, performance_rating=4, last_raise_months_ago=14,
last_equity_refresh_months_ago=None),
Employee("E002", "Yuki Tanaka", "Senior SWE (Frontend)", "L3", "Engineering", "Tier1",
base_salary=152_000, bonus_target_pct=0.0, equity_shares=30_000,
equity_strike=2.20, equity_current_409a=6.80, equity_vest_years_remaining=0.5,
benefits_annual=18_000, gender="F", ethnicity="Asian",
tenure_years=3.8, performance_rating=5, last_raise_months_ago=11,
last_equity_refresh_months_ago=30),
# Note: Yuki is high performer, near-vested, no recent refresh — flag expected
Employee("E003", "Marcus Johnson", "SWE II (Backend)", "L2", "Engineering", "Tier1",
base_salary=110_000, bonus_target_pct=0.0, equity_shares=15_000,
equity_strike=2.50, equity_current_409a=6.80, equity_vest_years_remaining=3.0,
benefits_annual=15_000, gender="M", ethnicity="Black",
tenure_years=1.2, performance_rating=3, last_raise_months_ago=12,
last_equity_refresh_months_ago=None),
# Note: Below band midpoint, recently hired — developing flag
Employee("E004", "Priya Nair", "Staff SWE", "L4", "Engineering", "Tier1",
base_salary=222_000, bonus_target_pct=0.0, equity_shares=60_000,
equity_strike=0.80, equity_current_409a=6.80, equity_vest_years_remaining=2.0,
benefits_annual=18_000, gender="F", ethnicity="Asian",
tenure_years=4.2, performance_rating=5, last_raise_months_ago=8,
last_equity_refresh_months_ago=8),
Employee("E005", "Tom Rivera", "SWE II (Platform)", "L2", "Engineering", "Tier2",
base_salary=88_000, bonus_target_pct=0.0, equity_shares=12_000,
equity_strike=3.00, equity_current_409a=6.80, equity_vest_years_remaining=2.5,
benefits_annual=14_000, gender="M", ethnicity="Hispanic",
tenure_years=1.8, performance_rating=4, last_raise_months_ago=22,
last_equity_refresh_months_ago=None),
# Note: No raise in 22 months, high performer — flag expected
Employee("E006", "Sarah Kim", "Eng Manager", "M1", "Engineering", "Tier1",
base_salary=192_000, bonus_target_pct=0.10, equity_shares=35_000,
equity_strike=1.20, equity_current_409a=6.80, equity_vest_years_remaining=1.8,
benefits_annual=18_000, gender="F", ethnicity="Asian",
tenure_years=2.8, performance_rating=4, last_raise_months_ago=9,
last_equity_refresh_months_ago=9),
# Sales
Employee("S001", "David Chen", "Account Executive (MM)", "L3", "Sales", "Tier1",
base_salary=105_000, bonus_target_pct=0.50, equity_shares=8_000,
equity_strike=3.50, equity_current_409a=6.80, equity_vest_years_remaining=2.0,
benefits_annual=15_000, gender="M", ethnicity="Asian",
tenure_years=1.5, performance_rating=3, last_raise_months_ago=15,
last_equity_refresh_months_ago=None),
Employee("S002", "Amara Osei", "AE (Mid-Market)", "L3", "Sales", "Tier1",
base_salary=98_000, bonus_target_pct=0.50, equity_shares=6_000,
equity_strike=3.50, equity_current_409a=6.80, equity_vest_years_remaining=2.5,
benefits_annual=15_000, gender="F", ethnicity="Black",
tenure_years=1.0, performance_rating=4, last_raise_months_ago=12,
last_equity_refresh_months_ago=None),
# Note: High performer, significantly below midpoint — flag expected
Employee("S003", "Jordan Blake", "Sales Manager", "M1", "Sales", "Tier1",
base_salary=155_000, bonus_target_pct=0.20, equity_shares=20_000,
equity_strike=2.00, equity_current_409a=6.80, equity_vest_years_remaining=1.5,
benefits_annual=16_000, gender="NB", ethnicity="White",
tenure_years=2.2, performance_rating=3, last_raise_months_ago=10,
last_equity_refresh_months_ago=10),
# Product
Employee("P001", "Nina Patel", "Senior PM", "L3", "Product", "Tier1",
base_salary=176_000, bonus_target_pct=0.10, equity_shares=22_000,
equity_strike=1.80, equity_current_409a=6.80, equity_vest_years_remaining=2.0,
benefits_annual=17_000, gender="F", ethnicity="Asian",
tenure_years=2.0, performance_rating=4, last_raise_months_ago=12,
last_equity_refresh_months_ago=12),
# G&A
Employee("G001", "Chris Mueller", "Finance Manager", "L3", "G&A", "Tier1",
base_salary=125_000, bonus_target_pct=0.10, equity_shares=10_000,
equity_strike=2.80, equity_current_409a=6.80, equity_vest_years_remaining=3.0,
benefits_annual=16_000, gender="M", ethnicity="White",
tenure_years=1.5, performance_rating=3, last_raise_months_ago=15,
last_equity_refresh_months_ago=None),
Employee("G002", "Fatima Al-Hassan", "HR Operations", "L2", "G&A", "Tier1",
base_salary=82_000, bonus_target_pct=0.08, equity_shares=5_000,
equity_strike=4.00, equity_current_409a=6.80, equity_vest_years_remaining=3.5,
benefits_annual=14_000, gender="F", ethnicity="Middle Eastern",
tenure_years=0.8, performance_rating=3, last_raise_months_ago=8,
last_equity_refresh_months_ago=None),
# Note: Below band minimum — critical flag expected
]
return roster
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def load_roster_from_json(path: str) -> CompRoster:
with open(path) as f:
data = json.load(f)
employees = [Employee(**e) for e in data.pop("employees", [])]
bands = [BandDefinition(**b) for b in data.pop("bands", [])]
roster = CompRoster(**data)
roster.employees = employees
roster.bands = bands
return roster
def main():
parser = argparse.ArgumentParser(
description="Compensation Benchmarker — salary analysis and pay equity audit",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python comp_benchmarker.py # Run sample roster
python comp_benchmarker.py --config roster.json # Load from JSON
python comp_benchmarker.py --export-csv # Output CSV
python comp_benchmarker.py --export-json # Output JSON template
"""
)
parser.add_argument("--config", help="Path to JSON roster file")
parser.add_argument("--export-csv", action="store_true", help="Export analysis as CSV")
parser.add_argument("--export-json", action="store_true", help="Export sample roster as JSON template")
args = parser.parse_args()
if args.config:
roster = load_roster_from_json(args.config)
else:
roster = build_sample_roster()
if args.export_json:
data = asdict(roster)
print(json.dumps(data, indent=2))
return
if args.export_csv:
print(export_csv(roster))
return
print_report(roster)
if __name__ == "__main__":
main()
FILE:chro-advisor/scripts/hiring_plan_modeler.py
#!/usr/bin/env python3
"""
Hiring Plan Modeler
===================
Builds hiring plans from business goals with cost projections.
Outputs quarterly headcount plan, cost model, and risk assessment.
Usage:
python hiring_plan_modeler.py # Run with built-in sample data
python hiring_plan_modeler.py --config plan.json # Load from JSON config
python hiring_plan_modeler.py --help
"""
import argparse
import json
import sys
from dataclasses import dataclass, field, asdict
from datetime import datetime, date
from typing import Optional
import csv
import io
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
@dataclass
class HireTarget:
"""One planned hire."""
role: str
level: str # L1, L2, L3, L4, M1, M2, M3, VP, C-Suite
function: str # Engineering, Sales, Product, G&A, Marketing, CS
quarter: str # Q1-2025, Q2-2025, etc.
base_salary: int # Annual, USD
bonus_pct: float # % of base (e.g., 0.10 for 10%)
equity_annual_usd: int # Annualized equity value at current 409A
benefits_annual: int # Employer-paid benefits
recruiter_fee_pct: float= 0.20 # Agency fee if used (0 for internal recruiter)
ramp_months: int = 3 # Months to full productivity
priority: str = "High" # High / Medium / Low
business_case: str = ""
open_to_internal: bool = False
@dataclass
class HiringPlan:
company: str
plan_period: str # e.g., "2025 Annual"
current_headcount: int
target_revenue: int # Annual target revenue ($)
current_revenue: int # Current ARR ($)
hires: list[HireTarget] = field(default_factory=list)
# Cost overheads beyond comp
overhead_rate: float = 0.25 # Workspace, software, onboarding overhead as % of base
internal_recruiter_cost: int = 0 # If you have an internal recruiter, annual cost
# ---------------------------------------------------------------------------
# Computation
# ---------------------------------------------------------------------------
def quarter_to_sortkey(q: str) -> tuple[int, int]:
"""Parse 'Q2-2025' → (2025, 2)"""
parts = q.upper().split("-")
if len(parts) == 2:
q_num = int(parts[0].replace("Q", ""))
year = int(parts[1])
return (year, q_num)
return (9999, 9)
def get_quarters(hires: list[HireTarget]) -> list[str]:
"""Return sorted unique quarters from hire list."""
quarters = sorted(set(h.quarter for h in hires), key=quarter_to_sortkey)
return quarters
def compute_hire_costs(hire: HireTarget) -> dict:
"""Compute total first-year cost for one hire."""
total_comp = hire.base_salary + int(hire.base_salary * hire.bonus_pct) + hire.equity_annual_usd + hire.benefits_annual
recruiter_fee = int(hire.base_salary * hire.recruiter_fee_pct)
overhead = int(hire.base_salary * 0.25) # workspace, tools, onboarding
ramp_productivity_cost = int(hire.base_salary * (hire.ramp_months / 12)) # cost during ramp
return {
"base_salary": hire.base_salary,
"target_bonus": int(hire.base_salary * hire.bonus_pct),
"equity_annual": hire.equity_annual_usd,
"benefits": hire.benefits_annual,
"total_comp": total_comp,
"recruiter_fee": recruiter_fee,
"overhead": overhead,
"ramp_cost": ramp_productivity_cost,
"first_year_total": total_comp + recruiter_fee + overhead,
"fully_loaded_first_year": total_comp + recruiter_fee + overhead + ramp_productivity_cost,
}
def summarize_by_quarter(plan: HiringPlan) -> dict[str, dict]:
"""Aggregate headcount and costs per quarter."""
quarters = get_quarters(plan.hires)
summary = {}
running_headcount = plan.current_headcount
for q in quarters:
q_hires = [h for h in plan.hires if h.quarter == q]
q_costs = [compute_hire_costs(h) for h in q_hires]
total_comp = sum(c["total_comp"] for c in q_costs)
total_first_year = sum(c["first_year_total"] for c in q_costs)
recruiter_fees = sum(c["recruiter_fee"] for c in q_costs)
running_headcount += len(q_hires)
summary[q] = {
"new_hires": len(q_hires),
"headcount_eop": running_headcount,
"total_annual_comp_added": total_comp,
"total_first_year_cost": total_first_year,
"recruiter_fees": recruiter_fees,
"hires": q_hires,
"costs": q_costs,
}
return summary
def summarize_by_function(plan: HiringPlan) -> dict[str, dict]:
"""Aggregate headcount and costs per function."""
functions: dict[str, dict] = {}
for hire in plan.hires:
fn = hire.function
if fn not in functions:
functions[fn] = {"count": 0, "total_comp": 0, "total_first_year": 0, "roles": []}
costs = compute_hire_costs(hire)
functions[fn]["count"] += 1
functions[fn]["total_comp"] += costs["total_comp"]
functions[fn]["total_first_year"] += costs["first_year_total"]
functions[fn]["roles"].append(hire.role)
return functions
def compute_totals(plan: HiringPlan) -> dict:
all_costs = [compute_hire_costs(h) for h in plan.hires]
total_hires = len(plan.hires)
total_comp = sum(c["total_comp"] for c in all_costs)
total_first_year = sum(c["first_year_total"] for c in all_costs)
total_fully_loaded = sum(c["fully_loaded_first_year"] for c in all_costs)
total_recruiter = sum(c["recruiter_fee"] for c in all_costs)
final_headcount = plan.current_headcount + total_hires
revenue_per_employee = plan.target_revenue / final_headcount if final_headcount > 0 else 0
revenue_per_employee_current = plan.current_revenue / plan.current_headcount if plan.current_headcount > 0 else 0
return {
"total_hires": total_hires,
"final_headcount": final_headcount,
"headcount_growth_pct": ((final_headcount - plan.current_headcount) / plan.current_headcount * 100) if plan.current_headcount > 0 else 0,
"total_annual_comp_added": total_comp,
"total_first_year_cost": total_first_year,
"total_fully_loaded_first_year": total_fully_loaded,
"total_recruiter_fees": total_recruiter,
"revenue_per_employee_target": revenue_per_employee,
"revenue_per_employee_current": revenue_per_employee_current,
"avg_comp_per_hire": total_comp // total_hires if total_hires > 0 else 0,
}
# ---------------------------------------------------------------------------
# Risk assessment
# ---------------------------------------------------------------------------
def assess_risks(plan: HiringPlan, totals: dict) -> list[dict]:
risks = []
# Headcount growth too fast
growth_pct = totals["headcount_growth_pct"]
if growth_pct > 80:
risks.append({
"severity": "HIGH",
"category": "Execution",
"finding": f"Headcount growing {growth_pct:.0f}% this period. "
"Culture and processes rarely scale this fast without breakage.",
"recommendation": "Stagger Q3/Q4 hires. Validate Q1/Q2 cohort is onboarded before next wave."
})
elif growth_pct > 50:
risks.append({
"severity": "MEDIUM",
"category": "Execution",
"finding": f"Headcount growing {growth_pct:.0f}% — significant scaling challenge.",
"recommendation": "Ensure onboarding infrastructure scales. Assign buddy/mentor to each hire."
})
# High concentration in one quarter
quarters = get_quarters(plan.hires)
q_counts = {q: sum(1 for h in plan.hires if h.quarter == q) for q in quarters}
max_q = max(q_counts.values()) if q_counts else 0
if max_q > len(plan.hires) * 0.5 and max_q > 4:
heavy_q = [q for q, c in q_counts.items() if c == max_q][0]
risks.append({
"severity": "MEDIUM",
"category": "Hiring Execution",
"finding": f"More than 50% of hires planned in {heavy_q} ({max_q} hires). "
"Recruiting capacity and onboarding bandwidth may be insufficient.",
"recommendation": "Spread hires across quarters. Hiring pipeline needs to start 60–90 days before target start date."
})
# Revenue per employee declining
if totals["revenue_per_employee_target"] < totals["revenue_per_employee_current"] * 0.7:
risks.append({
"severity": "HIGH",
"category": "Financial",
"finding": f"Revenue per employee declining from ,.0f to "
f",.0f — a {((totals['revenue_per_employee_target']/totals['revenue_per_employee_current'])-1)*100:.0f}% drop.",
"recommendation": "Validate that revenue model supports this headcount. Is target revenue achievable with this team?"
})
# Low priority hires consuming budget
low_priority_hires = [h for h in plan.hires if h.priority == "Low"]
if low_priority_hires:
lp_cost = sum(compute_hire_costs(h)["first_year_total"] for h in low_priority_hires)
risks.append({
"severity": "MEDIUM",
"category": "Prioritization",
"finding": f"{len(low_priority_hires)} 'Low' priority hires consuming ,.0f in first-year costs.",
"recommendation": "Consider deferring Low priority hires to preserve runway. Cut these first if budget tightens."
})
# Hires without business cases
no_case = [h for h in plan.hires if not h.business_case]
if no_case:
risks.append({
"severity": "MEDIUM",
"category": "Governance",
"finding": f"{len(no_case)} hires have no documented business case: {', '.join(h.role for h in no_case[:5])}{'...' if len(no_case) > 5 else ''}",
"recommendation": "Every hire over $80K should have a written business case. What revenue or risk does this role address?"
})
# High recruiter fee exposure
if totals["total_recruiter_fees"] > 100_000:
risks.append({
"severity": "LOW",
"category": "Cost",
"finding": f",.0f in recruiter fees. "
"Consider whether internal recruiter investment would be cheaper at this hiring volume.",
"recommendation": f"Internal recruiter at $120–150K fully loaded pays off at 3–4 hires/year vs. agency fees."
})
# No risks — that's itself a flag
if not risks:
risks.append({
"severity": "INFO",
"category": "General",
"finding": "No major risks flagged. Plan appears well-structured.",
"recommendation": "Validate assumptions: time-to-fill estimates, revenue model, and Q1 hiring pipeline status."
})
return risks
# ---------------------------------------------------------------------------
# Formatting / Output
# ---------------------------------------------------------------------------
def fmt(n: int) -> str:
return f",.0f"
def pct(n: float) -> str:
return f"{n:.1f}%"
def print_report(plan: HiringPlan):
WIDTH = 72
SEP = "=" * WIDTH
sep = "-" * WIDTH
print(SEP)
print(f" HIRING PLAN: {plan.company}")
print(f" Period: {plan.plan_period} | Generated: {date.today().isoformat()}")
print(SEP)
totals = compute_totals(plan)
q_summary = summarize_by_quarter(plan)
fn_summary = summarize_by_function(plan)
risks = assess_risks(plan, totals)
# Executive summary
print("\n[ EXECUTIVE SUMMARY ]")
print(sep)
print(f" Current headcount: {plan.current_headcount:>5}")
print(f" Planned hires: {totals['total_hires']:>5}")
print(f" Final headcount: {totals['final_headcount']:>5} (+{totals['headcount_growth_pct']:.0f}%)")
print(f" Current ARR: {fmt(plan.current_revenue):>12}")
print(f" Target revenue: {fmt(plan.target_revenue):>12}")
print(f" Revenue/employee now: {fmt(int(totals['revenue_per_employee_current'])):>12}")
print(f" Revenue/employee target: {fmt(int(totals['revenue_per_employee_target'])):>12}")
print()
print(f" Total annual comp added: {fmt(totals['total_annual_comp_added']):>12}")
print(f" Total first-year cost: {fmt(totals['total_first_year_cost']):>12}")
print(f" Fully loaded (w/ ramp): {fmt(totals['total_fully_loaded_first_year']):>12}")
print(f" Recruiter fees: {fmt(totals['total_recruiter_fees']):>12}")
print(f" Avg comp per hire: {fmt(totals['avg_comp_per_hire']):>12}")
# Quarterly breakdown
print(f"\n[ QUARTERLY HEADCOUNT PLAN ]")
print(sep)
print(f" {'Quarter':<10} {'New Hires':>10} {'HC (EOP)':>10} {'Comp Added':>14} {'1yr Cost':>14} {'Recruiter $':>12}")
print(f" {'-'*10} {'-'*10} {'-'*10} {'-'*14} {'-'*14} {'-'*12}")
for q, data in q_summary.items():
print(f" {q:<10} {data['new_hires']:>10} {data['headcount_eop']:>10} "
f"{fmt(data['total_annual_comp_added']):>14} "
f"{fmt(data['total_first_year_cost']):>14} "
f"{fmt(data['recruiter_fees']):>12}")
# By function
print(f"\n[ HEADCOUNT BY FUNCTION ]")
print(sep)
print(f" {'Function':<18} {'Hires':>7} {'Annual Comp':>14} {'1yr Cost':>14}")
print(f" {'-'*18} {'-'*7} {'-'*14} {'-'*14}")
for fn, data in sorted(fn_summary.items(), key=lambda x: -x[1]["count"]):
print(f" {fn:<18} {data['count']:>7} {fmt(data['total_comp']):>14} {fmt(data['total_first_year']):>14}")
# Hire detail
print(f"\n[ HIRE DETAIL ]")
print(sep)
print(f" {'Role':<30} {'Fn':<14} {'Lvl':<6} {'Q':<8} {'Base':>10} {'Total Comp':>12} {'Priority':<8}")
print(f" {'-'*30} {'-'*14} {'-'*6} {'-'*8} {'-'*10} {'-'*12} {'-'*8}")
for h in sorted(plan.hires, key=lambda x: quarter_to_sortkey(x.quarter)):
costs = compute_hire_costs(h)
print(f" {h.role:<30} {h.function:<14} {h.level:<6} {h.quarter:<8} "
f"{fmt(h.base_salary):>10} {fmt(costs['total_comp']):>12} {h.priority:<8}")
if h.business_case:
bc = h.business_case[:60] + "..." if len(h.business_case) > 60 else h.business_case
print(f" {'':>30} ↳ {bc}")
# Risk assessment
print(f"\n[ RISK ASSESSMENT ]")
print(sep)
sev_order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "INFO": 3}
for risk in sorted(risks, key=lambda r: sev_order.get(r["severity"], 99)):
sev = risk["severity"]
marker = {"HIGH": "⚠ HIGH", "MEDIUM": "◆ MED ", "LOW": "◇ LOW ", "INFO": "ℹ INFO"}[sev]
print(f"\n [{marker}] {risk['category']}")
# Wrap finding
finding = risk["finding"]
words = finding.split()
line = " Finding: "
for w in words:
if len(line) + len(w) + 1 > WIDTH - 2:
print(line)
line = " " + w + " "
else:
line += w + " "
if line.strip():
print(line)
reco = risk["recommendation"]
words = reco.split()
line = " Action: "
for w in words:
if len(line) + len(w) + 1 > WIDTH - 2:
print(line)
line = " " + w + " "
else:
line += w + " "
if line.strip():
print(line)
print(f"\n{SEP}\n")
def export_csv(plan: HiringPlan) -> str:
"""Return CSV of hire detail."""
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["Role", "Function", "Level", "Quarter", "Priority",
"Base Salary", "Bonus Target", "Equity Annual", "Benefits",
"Total Comp", "Recruiter Fee", "Overhead", "First Year Total",
"Ramp Months", "Open to Internal", "Business Case"])
for h in plan.hires:
c = compute_hire_costs(h)
writer.writerow([h.role, h.function, h.level, h.quarter, h.priority,
h.base_salary, c["target_bonus"], h.equity_annual_usd, h.benefits_annual,
c["total_comp"], c["recruiter_fee"], c["overhead"], c["first_year_total"],
h.ramp_months, h.open_to_internal, h.business_case])
return output.getvalue()
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
def build_sample_plan() -> HiringPlan:
"""Sample Series A → B hiring plan."""
plan = HiringPlan(
company="AcmeTech (Series A)",
plan_period="2025 Annual",
current_headcount=32,
current_revenue=3_500_000,
target_revenue=8_000_000,
overhead_rate=0.25,
internal_recruiter_cost=140_000,
)
plan.hires = [
# Q1 — Foundation hires
HireTarget(
role="Staff Software Engineer (Backend)",
level="L4", function="Engineering", quarter="Q1-2025",
base_salary=185_000, bonus_pct=0.0, equity_annual_usd=25_000,
benefits_annual=18_000, recruiter_fee_pct=0.0, ramp_months=2,
priority="High", open_to_internal=True,
business_case="Core API team is bottleneck for 3 roadmap items. Staff-level needed to lead architecture."
),
HireTarget(
role="Account Executive (Mid-Market)",
level="L3", function="Sales", quarter="Q1-2025",
base_salary=95_000, bonus_pct=0.50, equity_annual_usd=10_000,
benefits_annual=15_000, recruiter_fee_pct=0.18, ramp_months=4,
priority="High",
business_case="Pipeline coverage at 1.8x quota. Need 2.5x by Q2. AE adds $600K ARR/year at ramp."
),
HireTarget(
role="Product Designer (Senior)",
level="L3", function="Product", quarter="Q1-2025",
base_salary=145_000, bonus_pct=0.0, equity_annual_usd=18_000,
benefits_annual=18_000, recruiter_fee_pct=0.0, ramp_months=2,
priority="High",
business_case="Single designer for 4 squads. UX debt slowing enterprise deals requiring onboarding improvements."
),
# Q2 — Growth hires
HireTarget(
role="Engineering Manager (Frontend)",
level="M1", function="Engineering", quarter="Q2-2025",
base_salary=175_000, bonus_pct=0.10, equity_annual_usd=22_000,
benefits_annual=18_000, recruiter_fee_pct=0.20, ramp_months=3,
priority="High",
business_case="Frontend team at 7 ICs with no dedicated EM. Performance review debt is high; manager needed."
),
HireTarget(
role="Account Executive (Mid-Market)",
level="L2", function="Sales", quarter="Q2-2025",
base_salary=85_000, bonus_pct=0.50, equity_annual_usd=8_000,
benefits_annual=15_000, recruiter_fee_pct=0.18, ramp_months=4,
priority="High",
business_case="Second AE to reach 2.5x pipeline coverage target."
),
HireTarget(
role="Customer Success Manager",
level="L2", function="Customer Success", quarter="Q2-2025",
base_salary=90_000, bonus_pct=0.15, equity_annual_usd=8_000,
benefits_annual=15_000, recruiter_fee_pct=0.0, ramp_months=2,
priority="Medium",
business_case="CSM:account ratio at 1:60, industry standard 1:30. NRR has dipped 4pts in 2 quarters."
),
HireTarget(
role="Data Engineer",
level="L2", function="Engineering", quarter="Q2-2025",
base_salary=155_000, bonus_pct=0.0, equity_annual_usd=18_000,
benefits_annual=18_000, recruiter_fee_pct=0.0, ramp_months=3,
priority="Medium",
business_case="Analytics infrastructure blocking product analytics, customer dashboards, and board metrics."
),
# Q3 — Scale hires
HireTarget(
role="Senior Software Engineer (Backend)",
level="L3", function="Engineering", quarter="Q3-2025",
base_salary=165_000, bonus_pct=0.0, equity_annual_usd=20_000,
benefits_annual=18_000, recruiter_fee_pct=0.0, ramp_months=2,
priority="High",
business_case="Backend team needs capacity to deliver Q3 roadmap without delaying Q4 items."
),
HireTarget(
role="Head of Marketing",
level="M3", function="Marketing", quarter="Q3-2025",
base_salary=180_000, bonus_pct=0.15, equity_annual_usd=30_000,
benefits_annual=18_000, recruiter_fee_pct=0.20, ramp_months=3,
priority="High",
business_case="No marketing function. 100% of pipeline is outbound. Need inbound by Q1-2026 for Series B."
),
HireTarget(
role="People Operations Manager",
level="M1", function="G&A", quarter="Q3-2025",
base_salary=120_000, bonus_pct=0.10, equity_annual_usd=12_000,
benefits_annual=16_000, recruiter_fee_pct=0.0, ramp_months=2,
priority="Medium",
business_case="Founders spending 8hrs/week on HR ops at 40 employees. Unscalable. First dedicated HR hire."
),
# Q4 — Stretch hires (conditional on revenue milestone)
HireTarget(
role="Senior Software Engineer (Frontend)",
level="L3", function="Engineering", quarter="Q4-2025",
base_salary=160_000, bonus_pct=0.0, equity_annual_usd=18_000,
benefits_annual=18_000, recruiter_fee_pct=0.0, ramp_months=2,
priority="Medium",
business_case="Conditional on Q3 ARR exceeding $5.5M. Frontend team capacity planning for 2026 roadmap."
),
HireTarget(
role="Account Executive (Enterprise)",
level="L4", function="Sales", quarter="Q4-2025",
base_salary=120_000, bonus_pct=0.60, equity_annual_usd=15_000,
benefits_annual=15_000, recruiter_fee_pct=0.20, ramp_months=6,
priority="Low",
business_case="Enterprise motion exploratory. Requires ICP validation in Q2-Q3 before committing."
),
HireTarget(
role="DevOps / Platform Engineer",
level="L3", function="Engineering", quarter="Q4-2025",
base_salary=150_000, bonus_pct=0.0, equity_annual_usd=18_000,
benefits_annual=18_000, recruiter_fee_pct=0.0, ramp_months=3,
priority="Low",
business_case="Platform reliability becoming bottleneck. Conditional on uptime SLA breaches continuing in Q3."
),
]
return plan
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def load_plan_from_json(path: str) -> HiringPlan:
with open(path) as f:
data = json.load(f)
hires = [HireTarget(**h) for h in data.pop("hires", [])]
plan = HiringPlan(**data)
plan.hires = hires
return plan
def main():
parser = argparse.ArgumentParser(
description="Hiring Plan Modeler — build headcount plans with cost projections",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python hiring_plan_modeler.py # Run sample plan
python hiring_plan_modeler.py --config plan.json # Load from JSON
python hiring_plan_modeler.py --export-csv # Output CSV of hires
python hiring_plan_modeler.py --export-json # Output plan as JSON template
"""
)
parser.add_argument("--config", help="Path to JSON plan file")
parser.add_argument("--export-csv", action="store_true", help="Export hire detail as CSV")
parser.add_argument("--export-json", action="store_true", help="Export sample plan as JSON template")
args = parser.parse_args()
if args.config:
plan = load_plan_from_json(args.config)
else:
plan = build_sample_plan()
if args.export_json:
data = asdict(plan)
print(json.dumps(data, indent=2))
return
if args.export_csv:
print(export_csv(plan))
return
print_report(plan)
if __name__ == "__main__":
main()
FILE:ciso-advisor/SKILL.md
---
name: "ciso-advisor"
description: "Security leadership for growth-stage companies. Risk quantification in dollars, compliance roadmap (SOC 2/ISO 27001/HIPAA/GDPR), security architecture strategy, incident response leadership, and board-level security reporting. Use when building security programs, justifying security budget, selecting compliance frameworks, managing incidents, assessing vendor risk, or when user mentions CISO, security strategy, compliance roadmap, zero trust, or board security reporting."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: ciso-leadership
updated: 2026-03-05
python-tools: risk_quantifier.py, compliance_tracker.py
frameworks: risk-based-security, zero-trust, defense-in-depth
---
# CISO Advisor
Risk-based security frameworks for growth-stage companies. Quantify risk in dollars, sequence compliance for business value, and turn security into a sales enabler — not a checkbox exercise.
## Keywords
CISO, security strategy, risk quantification, ALE, SLE, ARO, security posture, compliance roadmap, SOC 2, ISO 27001, HIPAA, GDPR, zero trust, defense in depth, incident response, board security reporting, vendor assessment, security budget, cyber risk, program maturity
## Quick Start
```bash
python scripts/risk_quantifier.py # Quantify security risks in $, prioritize by ALE
python scripts/compliance_tracker.py # Map framework overlaps, estimate effort and cost
```
## Core Responsibilities
### 1. Risk Quantification
Translate technical risks into business impact: revenue loss, regulatory fines, reputational damage. Use ALE to prioritize. See `references/security_strategy.md`.
**Formula:** `ALE = SLE × ARO` (Single Loss Expectancy × Annual Rate of Occurrence). Board language: "This risk has $X expected annual loss. Mitigation costs $Y."
### 2. Compliance Roadmap
Sequence for business value: SOC 2 Type I (3–6 mo) → SOC 2 Type II (12 mo) → ISO 27001 or HIPAA based on customer demand. See `references/compliance_roadmap.md` for timelines and costs.
### 3. Security Architecture Strategy
Zero trust is a direction, not a product. Sequence: identity (IAM + MFA) → network segmentation → data classification. Defense in depth beats single-layer reliance. See `references/security_strategy.md`.
### 4. Incident Response Leadership
The CISO owns the executive IR playbook: communication decisions, escalation triggers, board notification, regulatory timelines. See `references/incident_response.md` for templates.
### 5. Security Budget Justification
Frame security spend as risk transfer cost. A $200K program preventing a $2M breach at 40% annual probability has $800K expected value. See `references/security_strategy.md`.
### 6. Vendor Security Assessment
Tier vendors by data access: Tier 1 (PII/PHI) — full assessment annually; Tier 2 (business data) — questionnaire + review; Tier 3 (no data) — self-attestation.
## Key Questions a CISO Asks
- "What's our crown jewel data, and who can access it right now?"
- "If we had a breach today, what's our regulatory notification timeline?"
- "Which compliance framework do our top 3 prospects actually require?"
- "What's our blast radius if our largest SaaS vendor is compromised?"
- "We spent $X on security last year — what specific risks did that reduce?"
## Security Metrics
| Category | Metric | Target |
|----------|--------|--------|
| Risk | ALE coverage (mitigated risk / total risk) | > 80% |
| Detection | Mean Time to Detect (MTTD) | < 24 hours |
| Response | Mean Time to Respond (MTTR) | < 4 hours |
| Compliance | Controls passing audit | > 95% |
| Hygiene | Critical patches within SLA | > 99% |
| Access | Privileged accounts reviewed quarterly | 100% |
| Vendor | Tier 1 vendors assessed annually | 100% |
| Training | Phishing simulation click rate | < 5% |
## Red Flags
- Security budget justified by "industry benchmarks" rather than risk analysis
- Certifications pursued before basic hygiene (patching, MFA, backups)
- No documented asset inventory — can't protect what you don't know you have
- IR plan exists but has never been tested (tabletop or live drill)
- Security team reports to IT, not executive level — misaligned incentives
- Single vendor for identity + endpoint + email — one breach, total exposure
- Security questionnaire backlog > 30 days — silently losing enterprise deals
## Integration with Other C-Suite Roles
| When... | CISO works with... | To... |
|---------|--------------------|-------|
| Enterprise sales | CRO | Answer questionnaires, unblock deals |
| New product features | CTO/CPO | Threat modeling, security review |
| Compliance budget | CFO | Size program against risk exposure |
| Vendor contracts | Legal/COO | Security SLAs and right-to-audit |
| M&A due diligence | CEO/CFO | Target security posture assessment |
| Incident occurs | CEO/Legal | Response coordination and disclosure |
## Detailed References
- `references/security_strategy.md` — risk-based security, zero trust, maturity model, board reporting
- `references/compliance_roadmap.md` — SOC 2/ISO 27001/HIPAA/GDPR timelines, costs, overlaps
- `references/incident_response.md` — executive IR playbook, communication templates, tabletop design
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- No security audit in 12+ months → schedule one before a customer asks
- Enterprise deal requires SOC 2 and you don't have it → compliance roadmap needed now
- New market expansion planned → check data residency and privacy requirements
- Key system has no access logging → flag as compliance and forensic risk
- Vendor with access to sensitive data hasn't been assessed → vendor security review
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Assess our security posture" | Risk register with quantified business impact (ALE) |
| "We need SOC 2" | Compliance roadmap with timeline, cost, effort, quick wins |
| "Prep for security audit" | Gap analysis against target framework with remediation plan |
| "We had an incident" | IR coordination plan + communication templates |
| "Security board section" | Risk posture summary, compliance status, incident report |
## Reasoning Technique: Risk-Based Reasoning
Evaluate every decision through probability × impact. Quantify risks in business terms (dollars, not severity labels). Prioritize by expected annual loss.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:ciso-advisor/references/compliance_roadmap.md
# Compliance Roadmap Reference
## Decision Framework: Which Framework First?
**Start here — who are your customers?**
```
Enterprise SaaS (B2B, US market) → SOC 2 Type II first
Healthcare / health data → HIPAA + SOC 2 together
EU customers or EU-resident data → GDPR (non-optional if applicable)
EU enterprise sales → ISO 27001 + GDPR
Government / defense → FedRAMP / CMMC (separate scope)
All of the above (Series B+) → Multi-framework efficiency approach
```
**The sequencing principle:** SOC 2 Type I is the fastest proof of intent (3–6 months). Type II is the credibility signal (12 months). Everything else builds on your control library.
---
## 1. SOC 2
### What It Is
SOC 2 is an attestation (not a certification) that your controls meet the AICPA Trust Service Criteria. An independent CPA firm audits your controls and issues a report.
- **Type I:** Controls are suitably designed at a point in time (snapshot). Lower credibility but faster.
- **Type II:** Controls operated effectively over a period of time (minimum 6 months). This is what enterprise buyers want.
### Trust Service Criteria (TSC)
You must include **Security** (CC). Others are optional:
| Criteria | When to add |
|---|---|
| Security (CC) | Always required |
| Availability | If uptime SLAs are contractual |
| Confidentiality | If you process confidential third-party data |
| Processing Integrity | If accuracy of processing is critical (fintech, data processing) |
| Privacy | If you make privacy commitments beyond GDPR/CCPA scope |
Most startups: **Security + Availability** is sufficient.
### Timeline: SOC 2 Type I
| Phase | Duration | Activities |
|---|---|---|
| Readiness assessment | 2–4 weeks | Gap analysis against CC criteria, identify control owners |
| Policy documentation | 4–6 weeks | Write ~15–20 policies (acceptable use, access control, change management, etc.) |
| Control implementation | 4–8 weeks | Deploy technical controls, fix gaps identified in readiness |
| Evidence collection | 2–4 weeks | Screenshots, logs, configs — auditor will sample these |
| Audit fieldwork | 2–4 weeks | CPA firm reviews evidence, interviews control owners |
| Report issuance | 2–4 weeks | Report issued, reviewed, shared with customers |
| **Total** | **3–6 months** | — |
### Timeline: SOC 2 Type II (after Type I)
| Phase | Duration | Notes |
|---|---|---|
| Observation period | 6–12 months | Controls must operate consistently — no exceptions |
| Audit fieldwork | 4–6 weeks | Auditor samples evidence across full period |
| Report issuance | 2–4 weeks | — |
| **Total from Type I** | **9–18 months** | Faster if Type I was clean |
### Cost Estimates
| Item | SOC 2 Type I | SOC 2 Type II |
|---|---|---|
| Audit firm fees | $15,000–$35,000 | $25,000–$60,000 |
| Compliance platform (Vanta, Drata, Secureframe) | $12,000–$30,000/yr | Same platform |
| External counsel / vCISO | $10,000–$30,000 | $5,000–$15,000 maintenance |
| Internal time (eng + ops) | 200–400 hours | 100–200 hours/yr |
| **Total first year** | **$40,000–$100,000** | **+$30,000–$75,000** |
**Cost optimization tips:**
- Use a compliance platform (Vanta, Drata, Secureframe) — automated evidence collection halves audit cost
- Choose a mid-tier audit firm; Big 4 is overkill for startups
- Type I and Type II with same auditor = continuity discount
### Common Failure Modes
1. Controls documented but not operating (access reviews on paper only)
2. Exceptions during observation period (one admin account without MFA = finding)
3. No formal security awareness training (required for CC criteria)
4. Change management not followed (no ticket for that production change)
5. Vendor risk management missing (you must assess your critical vendors)
---
## 2. ISO 27001
### What It Is
ISO 27001 is an internationally recognized certification for an Information Security Management System (ISMS). Unlike SOC 2, it's a certification (pass/fail), not an attestation report. Issued by accredited certification bodies (BSI, Bureau Veritas, DNV, TÜV).
**Why ISO 27001 over SOC 2:** EU enterprise buyers, government contracts, and global markets often prefer or require ISO 27001. It's geographically neutral.
### Scope Decision
ISO 27001 scope is flexible — you can certify a subset of the organization.
- **Narrow scope:** The production environment only — fastest, cheapest
- **Full scope:** Entire organization — most credibility, highest effort
- **Recommended for startups:** Production environment + key business processes
### Certification Timeline
| Phase | Duration | Activities |
|---|---|---|
| Gap analysis | 2–4 weeks | Assess current state vs. 93 controls in Annex A |
| ISMS design | 4–8 weeks | Scope, risk methodology, SoA (Statement of Applicability) |
| Policy and procedure development | 6–10 weeks | Mandatory documents: risk treatment plan, asset register, ISMS policy |
| Risk assessment | 4–6 weeks | Identify, analyze, evaluate risks; produce risk register |
| Control implementation | 8–16 weeks | Implement gaps from risk assessment |
| Internal audit | 2–4 weeks | First internal audit of ISMS |
| Management review | 1–2 weeks | Leadership sign-off on ISMS |
| Stage 1 audit (documentation) | 1–2 weeks | Certification body reviews docs and scope |
| Stage 2 audit (implementation) | 1–2 weeks | Certification body verifies controls are operating |
| Certification issued | 1–2 weeks | Certificate valid for 3 years with annual surveillance audits |
| **Total** | **9–18 months** | — |
### Cost Estimates
| Item | Cost |
|---|---|
| Certification body fees (Stage 1 + Stage 2) | $15,000–$40,000 |
| Annual surveillance audits | $8,000–$20,000/yr |
| vCISO / consultant (if not in-house) | $30,000–$80,000 |
| GRC platform | $10,000–$25,000/yr |
| Internal time | 400–800 hours |
| **Total first year** | **$55,000–$150,000** |
### Mandatory ISO 27001:2022 Documents
- ISMS scope document
- Information security policy
- Risk assessment methodology
- Risk register with risk treatment plan
- Statement of Applicability (SoA)
- Asset inventory
- Competence and awareness records
- Internal audit reports
- Management review minutes
- Nonconformity and corrective action records
---
## 3. HIPAA for Health Tech Startups
### When HIPAA Applies
HIPAA applies if you are a **Covered Entity** (healthcare provider, health plan, clearinghouse) or a **Business Associate** (you process, store, or transmit Protected Health Information on behalf of a Covered Entity).
**Key trigger:** If your product touches patient data in any way and a US healthcare provider uses your product, you are likely a Business Associate. You must sign a **BAA (Business Associate Agreement)** with each Covered Entity customer.
### HIPAA Rule Structure
| Rule | Focus | Key Requirements |
|---|---|---|
| Privacy Rule | How PHI can be used and disclosed | Minimum necessary, patient rights, notice of privacy practices |
| Security Rule | Technical and physical safeguards for ePHI | Required and addressable safeguards |
| Breach Notification Rule | What to do if PHI is breached | Timing and content of breach notifications |
### Security Rule: Required vs. Addressable
**Required safeguards** must be implemented exactly as specified. **Addressable safeguards** must be implemented or documented why an equivalent measure was used.
**Key Required Safeguards:**
- Unique user IDs (no shared logins)
- Emergency access procedure
- Audit controls (logging access to ePHI)
- Transmission security (encryption in transit)
- Person or entity authentication
**Key Addressable Safeguards (implement or document why not):**
- Automatic logoff
- Encryption and decryption (encryption at rest — despite being "addressable," regulators expect it)
- Audit review procedures
- Security reminders and training
### HIPAA Compliance Timeline
| Phase | Duration | Activities |
|---|---|---|
| Risk analysis | 4–6 weeks | Document all PHI flows, assess risks to PHI — **required by law** |
| Policy development | 4–8 weeks | Privacy policies, breach notification, workforce training |
| Technical safeguard implementation | 4–12 weeks | Encryption, audit logging, access controls, BAA templates |
| Workforce training | 2–4 weeks | Annual HIPAA training for all staff with PHI access |
| BAA execution | Ongoing | Execute with all vendors who process PHI |
| **Total** | **4–8 months** | — |
### Cost Estimates
| Item | Cost |
|---|---|
| Initial risk analysis (consultant) | $15,000–$40,000 |
| Policy development | $8,000–$20,000 |
| Technical implementation | $20,000–$60,000 |
| Annual training and maintenance | $5,000–$15,000/yr |
| HIPAA compliance platform | $10,000–$20,000/yr |
| **Total first year** | **$45,000–$130,000** |
### HIPAA Penalties (Why This Matters)
| Violation Category | Penalty per Violation | Annual Cap |
|---|---|---|
| Unaware | $100–$50,000 | $25,000 |
| Reasonable cause | $1,000–$50,000 | $100,000 |
| Willful neglect (corrected) | $10,000–$50,000 | $250,000 |
| Willful neglect (not corrected) | $50,000 | $1,500,000 |
---
## 4. GDPR Compliance Program
### When GDPR Applies
GDPR applies if you:
- Are established in the EU/EEA
- Process personal data of EU/EEA residents (regardless of your location)
- Offer goods or services to EU residents
- Monitor the behavior of EU residents
**Key point for US startups:** If you have EU users or EU employees, GDPR applies to you.
### Core GDPR Principles (Build These In)
1. **Lawfulness, fairness, transparency** — have a legal basis for every processing activity
2. **Purpose limitation** — collect data for specified, explicit purposes only
3. **Data minimization** — collect only what you need
4. **Accuracy** — keep data accurate
5. **Storage limitation** — delete data when no longer needed
6. **Integrity and confidentiality** — appropriate security measures
7. **Accountability** — demonstrate compliance
### Legal Bases for Processing
| Basis | When to use |
|---|---|
| Consent | Marketing, non-essential cookies, optional features |
| Contract | Processing necessary to deliver your service |
| Legitimate interests | Analytics, fraud prevention, security (requires LIA) |
| Legal obligation | Compliance with legal requirements |
| Vital interests | Emergency situations only |
**Avoid over-relying on consent** — it must be freely given, specific, informed, and unambiguous. Contractual basis is more robust for core product data.
### GDPR Compliance Checklist
**Governance:**
- [ ] Data Protection Officer (DPO) appointed (required for large-scale processing or sensitive data)
- [ ] Record of Processing Activities (RoPA) maintained
- [ ] Data Protection Impact Assessments (DPIA) for high-risk processing
**Rights Management (respond within 1 month):**
- [ ] Right of access (data subject access requests — DSARs)
- [ ] Right to rectification
- [ ] Right to erasure ("right to be forgotten")
- [ ] Right to data portability
- [ ] Right to object to processing
**Technical Measures:**
- [ ] Privacy by design in product development
- [ ] Data minimization enforced
- [ ] Encryption at rest and in transit
- [ ] Pseudonymization where possible
- [ ] Retention policies and automated deletion
**Vendor Management:**
- [ ] Data Processing Agreements (DPAs) with all processors
- [ ] Standard Contractual Clauses (SCCs) for non-EU transfers
**Breach Notification:**
- [ ] Notify supervisory authority within 72 hours of awareness
- [ ] Notify affected individuals if high risk to their rights and freedoms
### GDPR Compliance Timeline
| Phase | Duration | Activities |
|---|---|---|
| Data mapping | 3–6 weeks | Map all personal data flows: collect, store, process, share, delete |
| Legal basis review | 2–4 weeks | Assign legal basis to each processing activity |
| Policy updates | 4–6 weeks | Privacy policy, cookie policy, employee data notices |
| DPA execution | 2–4 weeks | Execute DPAs with all processors (SaaS vendors, cloud providers) |
| Technical controls | 4–12 weeks | Consent management, data subject rights automation, retention |
| Staff training | 2–4 weeks | GDPR awareness for all staff |
| **Total** | **3–6 months** | — |
### GDPR Fines
- **Standard violations:** Up to €10M or 2% of global annual revenue
- **Major violations** (basic principles, consent, data subject rights): Up to €20M or 4% of global annual revenue
- **Highest ever fine:** Meta, €1.2B (2023, data transfers to US)
---
## 5. Multi-Framework Efficiency
### Control Overlap Analysis
The same underlying controls satisfy multiple frameworks. Build once, certify multiple times.
**Core Control Domain Overlap:**
| Control Domain | SOC 2 | ISO 27001 | HIPAA | GDPR |
|---|---|---|---|---|
| Access control / IAM | CC6 | A.5.15–A.5.18 | §164.312(a) | Art. 32 |
| Encryption at rest/transit | CC6.7 | A.8.24 | §164.312(a)(2)(iv) | Art. 32 |
| Audit logging | CC7.2 | A.8.15, A.8.17 | §164.312(b) | Art. 32 |
| Incident response | CC7.3–CC7.5 | A.5.24–A.5.28 | §164.308(a)(6) | Art. 33–34 |
| Vendor/third-party mgmt | CC9 | A.5.19–A.5.22 | §164.308(b) | Art. 28 |
| Risk assessment | CC3 | Clause 6.1 | §164.308(a)(1) | Art. 32 |
| Security training | CC1.4 | A.6.3, A.6.8 | §164.308(a)(5) | Art. 39 |
| Business continuity | A1 | A.5.29–A.5.30 | §164.308(a)(7) | Art. 32 |
| Data classification | CC6.1 | A.5.9–A.5.13 | §164.514 | Art. 5(1)(c) |
| Change management | CC8 | A.8.32 | §164.312(c) | Art. 25 |
**Efficiency Rule:** If you build SOC 2 controls correctly, you're ~65–75% of the way to ISO 27001 and ~70% of the way to HIPAA. Don't rebuild — extend.
### Recommended Sequencing by Company Profile
**B2B SaaS (US-focused):**
```
Month 0–6: SOC 2 Type I → unblocks early enterprise deals
Month 6–18: SOC 2 Type II → enterprise table stakes
Month 18–30: ISO 27001 → EU market expansion
(GDPR should be woven in from month 0 if any EU data)
```
**HealthTech (US):**
```
Month 0–8: HIPAA compliance + BAA readiness → enables healthcare customers
Month 6–18: SOC 2 Type II → enterprise IT requirements on top of HIPAA
Month 18+: ISO 27001 if entering European market
```
**EU-founded SaaS:**
```
Month 0–3: GDPR compliance → legal requirement, not optional
Month 3–12: ISO 27001 → EU enterprise default expectation
Month 12–24: SOC 2 → US market expansion
```
**HealthTech (EU):**
```
Concurrent: GDPR + ISO 27001 (strong overlap with MDR/IVDR security requirements)
Month 12+: HIPAA if entering US market
```
### Shared Evidence Model
Build your evidence library once. Tag each piece of evidence by framework:
```
evidence/
├── access_control/
│ ├── iam_policy.pdf [SOC2:CC6, ISO:A5.15, HIPAA:164.312a]
│ ├── mfa_screenshot_Q1.png [SOC2:CC6, ISO:A8.5, HIPAA:164.312d]
│ └── access_review_log.xlsx [SOC2:CC6, ISO:A5.18, HIPAA:164.308a]
├── encryption/
│ ├── kms_config.png [SOC2:CC6.7, ISO:A8.24, HIPAA:164.312e]
│ └── tls_policy.md [SOC2:CC6.7, ISO:A8.24, HIPAA:164.312e]
└── incident_response/
├── ir_plan.pdf [SOC2:CC7, ISO:A5.24, HIPAA:164.308a6]
└── tabletop_log.pdf [SOC2:CC7, ISO:A5.26, HIPAA:164.308a6]
```
### GRC Platform Comparison
| Platform | Best For | Price/yr | SOC 2 | ISO 27001 | HIPAA | GDPR |
|---|---|---|---|---|---|---|
| Vanta | Fast SOC 2, US startups | $15–30K | ✅ | ✅ | ✅ | ✅ |
| Drata | Automation depth | $18–35K | ✅ | ✅ | ✅ | ✅ |
| Secureframe | Cost-effective | $10–20K | ✅ | ✅ | ✅ | ✅ |
| Sprinto | SMB, global | $12–25K | ✅ | ✅ | ✅ | ✅ |
| Tugboat Logic | Mid-market | $20–40K | ✅ | ✅ | ✅ | ✅ |
| Manual | Budget-constrained | $0 + time | ✅ | ✅ | ✅ | ✅ |
**Recommendation:** For Series A startups, Vanta or Drata pays for itself in reduced auditor fees and internal time savings. Budget $15–25K/year.
### Compliance Maintenance Annual Budget
| Item | SOC 2 | ISO 27001 | HIPAA | GDPR |
|---|---|---|---|---|
| Annual audit / surveillance | $25–60K | $8–20K | n/a (self-assessed) | n/a (self-assessed) |
| GRC platform | $15–30K | Shared | Shared | Shared |
| Annual training | $3–8K | Shared | Shared | Shared |
| Policy review | $2–5K | $2–5K | $2–5K | $2–5K |
| **Total ongoing** | **$45–103K/yr** | **+$10–25K/yr** | **+$5–15K/yr** | **+$5–15K/yr** |
FILE:ciso-advisor/references/incident_response.md
# Incident Response Reference (Executive Playbook)
This is the executive IR playbook — strategic decisions, communication, and leadership during incidents. For technical playbooks (containment procedures, forensics), see your SOC runbooks.
---
## 1. Incident Classification
### Severity Levels
| Severity | Definition | Examples | Response Time | Escalation |
|---|---|---|---|---|
| SEV-1 (Critical) | Confirmed breach, data exfil, ransomware, production down | Active ransomware, confirmed data theft, complete service outage | Immediate (< 1 hour) | CEO, board within 24 hrs |
| SEV-2 (High) | Suspected breach, significant security event, extended outage | Credential compromise suspected, DDoS, 4-hour+ outage | < 4 hours | CEO, legal within 48 hrs |
| SEV-3 (Medium) | Security event with limited impact, short outage | Phishing success (contained), brief outage, single system compromise | < 24 hours | CISO-owned, weekly rollup |
| SEV-4 (Low) | Minor security event, near-miss | Failed phishing attempt, minor policy violation | < 72 hours | Team-owned |
### Breach vs. Security Incident
**Security incident:** Unplanned event affecting security — may or may not involve data.
**Data breach:** Confirmed unauthorized access to personal data — triggers regulatory notification obligations.
**Critical distinction for response planning:** A ransomware attack is an incident. If data was exfiltrated before encryption, it's also a breach. Assume breach until proven otherwise.
---
## 2. Executive IR Plan
### Phase 1: Detection & Initial Assessment (0–2 hours for SEV-1)
**Immediate actions (CISO):**
1. Receive alert from SOC/monitoring system or team member report
2. Make initial severity classification — don't wait for perfect information
3. Activate incident response team (IR lead, legal counsel, comms lead)
4. Create incident war room (dedicated Slack channel, video bridge, shared document)
5. **Stop the clock** — document exact time of discovery (regulatory timelines start here)
6. Begin chain of custody documentation if forensics may be needed
**Executive notification trigger (within 1 hour for SEV-1):**
- Notify CEO: incident status, initial severity, IR team activated
- Put legal counsel on notice — don't wait to determine if breach occurred
- If public company: notify General Counsel immediately (potential disclosure obligations)
**What you do NOT do in Phase 1:**
- Do not notify customers yet (confirm scope first)
- Do not delete or modify any logs or systems (evidence preservation)
- Do not make public statements
- Do not speculate about cause or scope
### Phase 2: Containment & Assessment (2–24 hours for SEV-1)
**Executive decisions required:**
- **Scope authorization:** Approve IR firm engagement (have a retainer in place)
- **System isolation:** Authorize taking systems offline if needed (revenue vs. evidence tradeoff)
- **Evidence preservation:** Authorize forensic image capture
- **Communication timing:** When to notify customers/partners (legal drives this)
**Board notification (for SEV-1/2):**
- Notify board chair / audit committee chair within 24 hours for SEV-1
- Board notification format: what we know, what we don't know, what we're doing, next update time
- Do not speculate on financial impact in board notification until known
**Legal assessment (with counsel):**
- Determine if personal data was involved
- Identify applicable notification laws (GDPR 72-hour, state breach notification, HIPAA 60-day)
- Assess litigation risk (document with privilege from this point)
- Evaluate cyber insurance policy coverage and notification requirements
### Phase 3: Notification & Communication (24–72 hours for SEV-1)
**Notification decision matrix:**
| Audience | Trigger | Timeline | Owner |
|---|---|---|---|
| Board | SEV-1/2 confirmed | < 24 hours | CEO/CISO |
| Regulators (GDPR) | Personal data breach confirmed | < 72 hours from awareness | Legal + CISO |
| Regulators (HIPAA) | PHI breach confirmed | < 60 days (early notice to HHS ASAP) | Legal + CISO |
| State regulators (US) | State breach notification laws vary | 30–90 days depending on state | Legal |
| Enterprise customers | Data confirmed in scope | As soon as practical after legal review | CEO/CRO |
| All customers | Data potentially in scope | After regulators notified | CEO/Comms |
| Media | Proactive or reactive | After notifying affected parties | CEO/Comms |
| Cyber insurer | Incident confirmed | Per policy terms (often 48–72 hours) | CFO/Legal |
### Phase 4: Recovery (Ongoing)
**Executive decisions:**
- Approve recovery timeline and communicate to customers
- Determine customer compensation or remediation (if applicable)
- Authorize security improvements identified during incident
- Decide on public disclosure beyond mandatory reporting
### Phase 5: Post-Incident Review (Within 30 days)
Covered in Section 5 of this document.
---
## 3. Communication Templates
### Board/Executive Notification (Initial — Hour 1)
**Subject:** [CONFIDENTIAL] Security Incident — Immediate Notification
---
We have identified a security incident as of [DATE/TIME].
**Current status:** [Brief factual description — what we know happened]
**Severity assessment:** SEV-[1/2/3]
**What we do not yet know:**
- [List unknowns — scope of impact, whether data was accessed, root cause]
**Actions taken so far:**
- IR team activated at [time]
- Legal counsel notified
- [Specific containment actions if applicable]
**Next update:** [Specific time, e.g., "in 4 hours or when we have material new information"]
**Who is managing this:** [CISO name] leads technical response; [CEO name] owns executive decisions. Contact: [CISO mobile]
---
### Customer Notification (After Legal Review)
**Subject:** Important Security Notice — [Company Name]
---
We are writing to inform you of a security incident that may have affected your data.
**What happened:**
On [DATE], we detected [brief, factual description of the incident — e.g., "unauthorized access to our systems"]. We identified this on [DISCOVERY DATE] and immediately launched an investigation.
**What information was involved:**
Based on our investigation, the following types of information may have been accessed: [list data types — e.g., names, email addresses, [if applicable: payment card information]].
Your [specific data types] [were / were not] affected.
**What we are doing:**
We have [list specific actions: engaged leading cybersecurity firm, notified relevant authorities, implemented additional security controls, etc.].
**What you can do:**
- [Specific actionable steps for customers]
- Monitor your accounts for unusual activity
- [If passwords: reset your password at X]
- [If payment data: contact your bank to monitor for unauthorized charges]
- Contact our dedicated support line at [contact] with any concerns
**For more information:**
We have set up a dedicated resource page at [URL]. Our support team is available at [contact].
We take the security of your data extremely seriously and deeply regret this incident occurred.
[CEO/CISO Name]
[Title], [Company Name]
---
### Regulator Notification — GDPR (72-hour requirement)
**To:** [Relevant Supervisory Authority — e.g., BfDI (Germany), CNIL (France), ICO (UK)]
**Subject:** Personal Data Breach Notification — [Company Name] — [Reference Number if applicable]
---
**1. Nature of the breach:**
[Description of what occurred, including how it happened]
**2. Categories and approximate number of data subjects concerned:**
[e.g., "Approximately [X] customers whose [name, email, account data] may have been accessed"]
**3. Categories and approximate number of personal data records concerned:**
[e.g., "Approximately [X] records containing [data categories]"]
**4. Likely consequences of the breach:**
[Risk assessment: what harm could data subjects face?]
**5. Measures taken or proposed:**
[Containment actions, remediation plan, customer notification plan]
**6. Contact details of the Data Protection Officer or other contact point:**
[Name, role, email, phone]
**Note:** This is an initial notification; we will provide supplemental information as our investigation continues.
---
### Media Statement (Reactive — When Contacted)
"[Company Name] is aware of a security incident that we identified on [date]. We immediately activated our incident response team and launched a comprehensive investigation. We have notified affected customers and relevant regulatory authorities as required. The security and privacy of our customers' data is our top priority, and we are committed to transparency as our investigation proceeds. We will provide updates at [URL]. We cannot provide additional details at this time to protect the integrity of our investigation."
**What not to say to media:**
- Number of affected users (until confirmed and disclosed to customers first)
- Cause of the incident (until investigation is complete)
- Financial impact (speculation creates liability)
- Anything that could be construed as minimizing the incident
---
## 4. Tabletop Exercise Design
### Purpose
Test the decision-making and communication processes — not the technical response. The goal is to surface gaps in escalation, communication, and judgment before a real incident.
### Recommended Frequency
- Annual full tabletop (2–3 hours, full leadership team)
- Semi-annual mini-tabletop (45 minutes, CISO + legal + CEO)
- Quarterly technical team exercise (separate from executive tabletop)
### Sample Tabletop Scenario: Ransomware
**Setup (read to participants):**
> It's 6:47 AM on a Monday. Your DevOps engineer receives automated alerts that production databases are inaccessible. By 7:15 AM, they discover a ransomware note demanding $500,000 in Bitcoin. Several files are already encrypted. Your last verified backup was 48 hours ago. Your business is B2B SaaS serving 200 enterprise customers. You process customer financial data.
**Discussion questions (timed, 10 minutes each):**
1. First 30 minutes — who do you call, in what order? Who decides whether to take production offline?
2. Legal assessment — what regulatory obligations have been triggered? What's the timeline?
3. Hour 4 — initial forensics suggests data may have been exfiltrated before encryption. How does your response change?
4. Customer communication — how do you communicate with enterprise customers who are asking for status?
5. Hour 24 — do you pay the ransom? Who makes this decision? What's the decision framework?
6. The press has found out and a reporter is calling. What do you say?
7. Day 5 — what's your board communication strategy?
**Post-discussion captures:**
- What decisions were unclear (ownership ambiguous)?
- What information did you need but didn't have?
- What processes did not exist that should?
- What would you do differently in the first hour?
### Sample Tabletop Scenario: Insider Threat
**Setup:**
> HR notifies you that an engineer was terminated this morning for performance reasons. 24 hours later, your SIEM generates an alert that this former employee's credentials accessed your customer database 30 minutes before their offboarding was complete. They downloaded 50,000 customer records. You don't know if they shared or sold the data.
**Key decision points:**
- When does this become a breach vs. a security incident?
- Do you notify customers? When?
- What are your legal options against the former employee?
- How do you handle this with the rest of the engineering team?
---
## 5. Post-Incident Review Framework
### Timeline
Conduct within 30 days of incident resolution. Do not delay — memory fades and teams move on.
### Blameless Post-Mortem Principles
The purpose is to improve systems and processes, not punish individuals. A blame culture means the next incident gets hidden longer.
### Post-Incident Review Structure
**1. Incident Timeline (factual, no editorializing)**
- Hour-by-hour reconstruction from detection to resolution
- Source: logs, Slack messages, incident ticket, war room notes
**2. Root Cause Analysis**
Use the "5 Whys" technique — keep asking why until you reach a systemic root cause, not a human error.
Example:
- Why was there a breach? → Attacker compromised an admin account
- Why was the admin account compromised? → Credentials stolen via phishing
- Why did phishing succeed? → User wasn't trained on this attack type
- Why wasn't training current? → Training program hadn't been updated in 18 months
- Why hadn't it been updated? → No owner was assigned to maintain the training program
- **Root cause: No assigned ownership for security training maintenance**
**3. What Went Well**
- Detection mechanisms that worked
- Response actions that contained damage
- Communication that was effective
- Teams that exceeded expectations
**4. What Needs Improvement**
- Detection gaps (how could we have found this faster?)
- Response gaps (what slowed us down?)
- Communication gaps (who didn't know what, when?)
- Process gaps (what didn't we have documented?)
**5. Action Items (with owners and deadlines)**
| Action | Owner | Due Date | Priority |
|---|---|---|---|
| [Specific improvement] | [Name] | [Date] | [P0/P1/P2] |
**6. Metrics Review**
- MTTD (Mean Time to Detect): [actual] vs. [target]
- MTTR (Mean Time to Respond): [actual] vs. [target]
- Customer impact: [affected customers, duration]
- Financial impact: [direct costs, revenue impact]
- Regulatory impact: [notifications sent, fines if any]
---
## 6. Insurance and Legal Considerations
### Cyber Insurance
**What to have before an incident:**
- Cyber liability policy with minimum $2M coverage (Series A); $5M+ (Series B+)
- Coverage should include: first-party loss, third-party liability, ransomware, business interruption, regulatory defense
- Pre-approved IR firms on your policy (using an approved firm can expedite claims)
- Notification requirements — know your insurer's required timeline (typically 48–72 hours)
**Policy exclusions to watch:**
- "War exclusion" — increasingly contested for nation-state attacks (NotPetya precedent)
- "Systemic risk" — some policies exclude widespread events affecting many insureds simultaneously
- "Prior acts" — incidents that began before policy inception
- "Failure to maintain reasonable security" — don't give your insurer a reason to deny
**Premium factors:**
- Revenue and data volume
- Security control maturity (MFA, EDR, backup, patch management)
- Industry (healthcare, financial services = higher premium)
- Claims history
**Ballpark premiums:**
- Seed/Series A ($1–10M ARR): $8,000–$25,000/yr
- Series B ($10–50M ARR): $25,000–$75,000/yr
- Series C+ ($50M+ ARR): $75,000–$250,000/yr
### Legal Counsel
**Have on retainer before an incident:**
- Cybersecurity/privacy attorney — breach notification, regulatory response
- General counsel — contracts, employment law (insider threats), litigation
- Consider: a law firm with data breach notification experience by jurisdiction
**Attorney-client privilege:** Once legal counsel is involved in an incident, communications and work product may be privileged. Engage counsel early to maximize privilege protection.
**Key legal decisions during an incident:**
- When does notification obligation clock start? (Legal determines this)
- Is this a breach or an incident? (Legal + CISO together)
- Who are the affected data subjects? (Legal + technical together)
- Do we pay the ransom? (Legal, CEO, board — never CISO alone)
- Do we cooperate with law enforcement? (Legal decision, involves trade-offs)
### Law Enforcement
**FBI Internet Crime Complaint Center (IC3):** File a complaint for ransomware or significant cybercrime. Does not obligate you to cooperate but creates a record.
**Pros of law enforcement involvement:**
- Access to threat intelligence they may have
- May recover funds in some cases (rare)
- Demonstrates good-faith response to regulators
**Cons of law enforcement involvement:**
- Loss of control over investigation timeline
- Potential for public disclosure if case pursued
- Slows ransom payment decisions (if considering)
- May create discovery obligations in litigation
**CISO recommendation:** Notify legal before contacting law enforcement. In most cases, file an IC3 complaint but don't actively engage FBI investigation unless there's a clear benefit.
FILE:ciso-advisor/references/security_strategy.md
# Security Strategy Reference
## 1. Risk-Based Security (Not Compliance-First)
### The Problem with Compliance-First Security
Most startups build security backwards: they get a compliance requirement (SOC 2, ISO 27001) and treat it as the security program. This produces:
- Controls that pass audits but don't reduce actual risk
- Resources allocated to documentation over protection
- Security teams optimizing for auditor satisfaction, not threat reduction
- False confidence ("we passed our audit") before real security exists
**The right order:**
1. Identify your actual threats (what do adversaries want from you?)
2. Identify your crown jewels (what's worth protecting most?)
3. Implement controls that address those threats to those assets
4. Map existing controls to compliance requirements — most overlap naturally
### Risk Identification Framework
**Asset Classification:**
```
Tier 1 — Crown Jewels
├── Customer PII/PHI
├── Payment card data
├── Intellectual property (source code, models, trade secrets)
└── Authentication credentials and secrets
Tier 2 — Business Critical
├── Internal communications (Slack, email)
├── Financial systems and data
├── Employee data
└── Business strategy documents
Tier 3 — Operational
├── Internal tooling and infrastructure configs
├── Non-sensitive operational data
└── Public-facing content and marketing
```
**Threat Actor Profiling:**
| Threat Actor | Motivation | Typical TTPs | Relative Likelihood |
|---|---|---|---|
| Financially motivated criminals | Data theft, ransomware | Phishing, credential stuffing | High |
| Nation-state | IP theft, espionage | Spear phishing, supply chain | Low-Medium (sector-dependent) |
| Insider threat | Financial gain, revenge | Privilege abuse, data exfil | Medium |
| Script kiddies | Notoriety, fun | Known CVEs, scanning | High (low sophistication) |
| Competitors | IP theft | Social engineering, insider recruitment | Low-Medium |
### Risk Quantification (FAIR Model Simplified)
**Annual Loss Expectancy:**
```
ALE = SLE × ARO
SLE (Single Loss Expectancy) = Asset Value × Exposure Factor
ARO (Annual Rate of Occurrence) = historical frequency or industry estimate
```
**Business Impact Categories:**
- **Direct financial loss**: fraud, ransomware payment, theft
- **Regulatory fines**: GDPR (4% global revenue), HIPAA ($100–$50K per violation), PCI DSS
- **Revenue impact**: customer churn post-breach, deal loss during incident, downtime cost
- **Reputational damage**: brand devaluation (harder to quantify, but real)
- **Legal costs**: incident response counsel, class action defense, settlements
**Example Risk Quantification:**
| Risk Scenario | SLE | ARO | ALE |
|---|---|---|---|
| Customer data breach (10K records) | $850K | 0.15 | $127,500/yr |
| Ransomware attack | $350K | 0.20 | $70,000/yr |
| Credential compromise + fraud | $120K | 0.35 | $42,000/yr |
| Third-party SaaS breach | $95K | 0.25 | $23,750/yr |
| Insider data exfiltration | $180K | 0.10 | $18,000/yr |
**Mitigation ROI:**
```
ROSI = (Risk Reduction × ALE) - Control Cost
────────────────────────────────────
Control Cost
Example: MFA deployment
Risk reduction: 99% for credential attacks
ALE reduced: $42,000 × 0.99 = $41,580
Control cost: $5,000/yr
ROSI: ($41,580 - $5,000) / $5,000 = 731%
```
---
## 2. Zero Trust Architecture at Strategy Level
### What Zero Trust Actually Means
Zero trust is not a product — it's an architectural principle: **never trust, always verify, assume breach.**
The traditional perimeter model (trust inside the network, distrust outside) fails because:
- Remote work destroyed the perimeter
- Cloud infrastructure has no perimeter
- 80% of breaches involve privileged account abuse (internal trust abused)
- Supply chain attacks compromise trusted software
### Zero Trust Maturity Model
**Stage 1 — Identity-Centric (Start Here)**
- MFA enforced for all users, all applications
- Identity provider (Okta, Azure AD, Google Workspace) as single control plane
- No shared service accounts
- Privileged Access Management (PAM) for admin access
- **Cost:** $20–80K/year | **Timeline:** 3–6 months
**Stage 2 — Device Trust**
- Endpoint detection and response (EDR) on all devices
- Device health checks before granting access
- Mobile device management (MDM) for BYOD
- Certificate-based device authentication
- **Cost:** $30–60K/year additional | **Timeline:** 6–12 months
**Stage 3 — Network Micro-Segmentation**
- Replace VPN with Zero Trust Network Access (ZTNA)
- Segment production from development from corporate
- East-west traffic inspection (not just north-south)
- **Cost:** $40–100K/year additional | **Timeline:** 12–18 months
**Stage 4 — Application-Level Controls**
- Just-in-time access (no standing privileges)
- Workload identity for service-to-service auth
- API gateway with authentication enforcement
- Continuous authorization (not just at login)
- **Cost:** $50–150K/year additional | **Timeline:** 18–30 months
**Strategic Guidance:**
- Don't sell zero trust as a project. It's a 3–5 year direction.
- Start with identity. It gives the most risk reduction per dollar.
- Measure progress by % of access covered by MFA, % of apps behind IdP, privilege account count.
---
## 3. Defense in Depth for Startups
### The Layered Security Model
```
Layer 1: Governance & Policies
└── Asset inventory, acceptable use, vendor management
Layer 2: Perimeter Controls
└── WAF, DDoS protection, email security (DMARC/DKIM/SPF)
Layer 3: Identity & Access
└── MFA, SSO, PAM, just-in-time access, least privilege
Layer 4: Endpoint Security
└── EDR, device management, patch management
Layer 5: Application Security
└── SAST/DAST, dependency scanning, code review, API security
Layer 6: Data Protection
└── Encryption at rest and in transit, DLP, backup/recovery
Layer 7: Detection & Response
└── SIEM/SOAR, log aggregation, alerting, incident response
Layer 8: Recovery
└── Backup testing, DR plan, RTO/RPO targets
```
### Startup Security Budget Allocation (Guidance)
| Stage | Annual Revenue | Recommended Security Budget | Priority Spend |
|---|---|---|---|
| Pre-seed/Seed | <$1M | 3–5% opex or $50–100K | MFA, backups, basic EDR |
| Series A | $1–10M | 2–4% revenue | +SIEM, SOC 2 Type I, AppSec |
| Series B | $10–50M | 3–5% revenue | +ZTNA, Red team, dedicated CISO |
| Series C+ | $50M+ | 4–6% revenue | +SOC, threat intelligence, M&A security |
**Non-negotiables regardless of stage:**
1. MFA on everything (particularly email, cloud consoles, code repos)
2. Automated backups with tested restore (ransomware defense)
3. Secrets management (no hardcoded credentials)
4. Dependency vulnerability scanning in CI/CD
5. Incident response plan (even a 2-page doc is better than nothing)
---
## 4. Security Program Maturity Model
**Based on NIST CSF and CMMI, simplified for startup context:**
### Level 1: Initial
- No formal policies
- Reactive security (respond to incidents, not prevent them)
- No dedicated security personnel
- Basic hygiene gaps (unpatched systems, shared passwords)
- **Typical:** Pre-seed, <20 employees
### Level 2: Developing
- Written security policies (even if not fully followed)
- Dedicated security responsibility (often part-time or dual-role)
- MFA deployed, basic asset inventory
- Incident response process documented
- SOC 2 Type I achievable from here in ~6 months
- **Typical:** Series A, 20–50 employees
### Level 3: Defined
- Security integrated into SDLC
- Dedicated security lead or vCISO
- Regular vulnerability scanning and patching
- Security awareness training program
- SOC 2 Type II and ISO 27001 achievable
- **Typical:** Series B, 50–150 employees
### Level 4: Managed
- Risk-based security program with quantified risks
- Security metrics reported to board quarterly
- Threat intelligence program
- Dedicated security team (3–8 people)
- Red team / penetration testing annually
- **Typical:** Series C+, 150–500 employees
### Level 5: Optimized
- Continuous monitoring and automated response
- Proactive threat hunting
- Industry leadership on security (bug bounty, disclosure program)
- Security as competitive advantage in sales
- **Typical:** Public company or regulated enterprise
### Maturity Assessment Questions
1. Can you list all systems that process customer data right now?
2. How long would it take to detect if an admin credential was compromised?
3. When was your last backup tested with a restore?
4. Do developers run any security checks before code is deployed?
5. Does the board receive security reporting? What's in it?
Score: 0 = no/don't know, 1 = partially, 2 = yes/verified
- 0–3: Level 1–2
- 4–7: Level 2–3
- 8–10: Level 3–4
---
## 5. Board-Level Security Reporting
### What the Board Cares About
Boards are not interested in CVE counts or firewall rules. They care about:
1. **Risk posture:** Are we getting better or worse?
2. **Regulatory exposure:** What fines could we face?
3. **Incident readiness:** If we're breached, are we prepared?
4. **Competitive position:** Do customers trust us with their data?
5. **Budget adequacy:** Are we investing appropriately?
### Quarterly Board Security Report Structure
**Executive Summary (1 page max)**
- Security posture score vs. last quarter (directional trend matters more than absolute)
- Top 3 risks and their business impact in dollars
- Key accomplishments this quarter
- Investment requested (if any)
**Risk Dashboard**
```
Risk Register Summary:
├── Critical (>$500K ALE): [count] risks, [count] mitigated
├── High ($100K–$500K ALE): [count] risks, [count] mitigated
├── Medium ($10K–$100K ALE): [count] risks
└── Low (<$10K ALE): [count] risks (for awareness only)
Trend: ↑ Risk exposure vs. Q[n-1] / ↓ Risk exposure vs. Q[n-1]
```
**Compliance Status**
- Framework certifications in scope and current status
- Next audit date
- Any findings from last audit and remediation status
**Incident Summary**
- Security incidents last quarter (count and severity)
- Time to detect / time to respond (vs. targets)
- Any regulatory reporting obligations triggered
**Key Metrics (4–6 max)**
- MFA adoption rate
- Critical patch SLA compliance
- Phishing simulation click rate (trend)
- Vendor assessments completed
**Budget Summary**
- Spend vs. budget
- Headcount
- Next quarter key investments and rationale
### Common Board Questions to Prepare For
- "Have we been breached?" (Know your detection capability, not just your answer)
- "How do we compare to peers?" (Benchmarks from Verizon DBIR, industry ISACs)
- "What's the one thing we should invest in?" (Have a clear answer)
- "If we're acquired, what would security due diligence find?" (Be honest)
- "What keeps you up at night?" (Have a real answer, not a vague one)
---
## 6. Security as Revenue Enabler
### The Sales Angle
For B2B companies, security certifications directly impact revenue:
- Enterprise buyers require SOC 2 as table stakes (increasingly SOC 2 Type II)
- Government and healthcare require ISO 27001 or HIPAA
- Passing security questionnaires faster closes deals faster
- A breach costs 10–30% customer churn; security investment is churn prevention
**How to Measure:**
- Deals blocked by security questionnaire failures (track in CRM)
- Average security questionnaire turnaround time
- Customer security reviews passed vs. failed
- Revenue attributed to new compliance certifications
### The Trust Narrative
Position security certifications in marketing:
- SOC 2 Type II: "Independently audited security controls, verified annually"
- ISO 27001: "Internationally certified information security management"
- HIPAA BAA: "Healthcare data protection to regulatory standards"
These aren't just compliance — they're trust signals that compress the sales cycle.
FILE:ciso-advisor/scripts/compliance_tracker.py
#!/usr/bin/env python3
"""
CISO Compliance Tracker
========================
Tracks compliance requirements across SOC 2, ISO 27001, HIPAA, and GDPR.
Shows control overlaps, estimates effort and cost, and prioritizes by business value.
Usage:
python compliance_tracker.py # Run with sample data
python compliance_tracker.py --json # JSON output
python compliance_tracker.py --csv output.csv # Export CSV
python compliance_tracker.py --framework soc2 # Show single framework
python compliance_tracker.py --gap-analysis # Show unaddressed requirements
python compliance_tracker.py --roadmap # Show sequenced roadmap
"""
import json
import csv
import sys
import argparse
from datetime import datetime, date
from typing import Optional
# ─── Framework Definitions ───────────────────────────────────────────────────
FRAMEWORKS = {
"soc2": {
"name": "SOC 2 Type II",
"full_name": "AICPA Trust Service Criteria — Security",
"typical_timeline_months": 12,
"typical_cost_usd": 65_000, # Audit + platform
"annual_maintenance_usd": 40_000,
"business_value": "Enterprise sales unblock, US market table stakes",
"mandatory_for": ["B2B SaaS selling to enterprise US companies"],
},
"iso27001": {
"name": "ISO 27001:2022",
"full_name": "Information Security Management System",
"typical_timeline_months": 15,
"typical_cost_usd": 95_000,
"annual_maintenance_usd": 30_000,
"business_value": "EU enterprise sales, global credibility",
"mandatory_for": ["EU enterprise customers", "Government contracts"],
},
"hipaa": {
"name": "HIPAA",
"full_name": "Health Insurance Portability and Accountability Act",
"typical_timeline_months": 7,
"typical_cost_usd": 75_000,
"annual_maintenance_usd": 20_000,
"business_value": "Healthcare customer access, BAA execution",
"mandatory_for": ["Business Associates", "Companies handling PHI"],
},
"gdpr": {
"name": "GDPR",
"full_name": "General Data Protection Regulation (EU) 2016/679",
"typical_timeline_months": 5,
"typical_cost_usd": 45_000,
"annual_maintenance_usd": 15_000,
"business_value": "EU market access, legal compliance",
"mandatory_for": ["EU-based companies", "Any company with EU user data"],
},
}
# ─── Control Domain Library ──────────────────────────────────────────────────
def build_control_domain(
domain_id: str,
name: str,
description: str,
soc2_ref: Optional[str],
iso27001_ref: Optional[str],
hipaa_ref: Optional[str],
gdpr_ref: Optional[str],
effort_days: int, # Estimated implementation effort in person-days
cost_usd: int, # Estimated implementation cost (tooling + time)
implementation_notes: str,
status: str = "Not Started", # Not Started | In Progress | Implemented | Verified
owner: Optional[str] = None,
target_date: Optional[str] = None,
) -> dict:
"""Build a control domain record."""
frameworks_applicable = []
if soc2_ref:
frameworks_applicable.append("soc2")
if iso27001_ref:
frameworks_applicable.append("iso27001")
if hipaa_ref:
frameworks_applicable.append("hipaa")
if gdpr_ref:
frameworks_applicable.append("gdpr")
return {
"domain_id": domain_id,
"name": name,
"description": description,
"references": {
"soc2": soc2_ref,
"iso27001": iso27001_ref,
"hipaa": hipaa_ref,
"gdpr": gdpr_ref,
},
"frameworks_applicable": frameworks_applicable,
"framework_count": len(frameworks_applicable),
"effort_days": effort_days,
"cost_usd": cost_usd,
"implementation_notes": implementation_notes,
"status": status,
"owner": owner,
"target_date": target_date,
}
def load_control_library() -> list[dict]:
"""
Core control domains mapped across SOC 2, ISO 27001, HIPAA, and GDPR.
Each domain represents a logical grouping of controls.
"""
controls = []
controls.append(build_control_domain(
domain_id="IAM-001",
name="Identity and Access Management",
description=(
"Unique user identities, MFA enforcement, SSO, least privilege access, "
"role-based access control, access provisioning and de-provisioning workflows."
),
soc2_ref="CC6.1, CC6.2, CC6.3",
iso27001_ref="A.5.15, A.5.16, A.5.17, A.5.18",
hipaa_ref="§164.312(a)(2)(i), §164.308(a)(3)",
gdpr_ref="Art. 32(1)(b)",
effort_days=15,
cost_usd=25_000, # SSO + MFA tooling
implementation_notes=(
"Deploy IdP (Okta/Azure AD/Google Workspace). Enforce MFA on all applications. "
"Document access provisioning process. Implement quarterly access reviews."
),
status="In Progress",
owner="IT/Security",
))
controls.append(build_control_domain(
domain_id="ENC-001",
name="Encryption at Rest and in Transit",
description=(
"Encryption of sensitive data stored in databases, file systems, and backups. "
"TLS 1.2+ for all data in transit. Key management and rotation."
),
soc2_ref="CC6.7",
iso27001_ref="A.8.24",
hipaa_ref="§164.312(a)(2)(iv), §164.312(e)(2)(ii)",
gdpr_ref="Art. 32(1)(a)",
effort_days=10,
cost_usd=8_000,
implementation_notes=(
"Enable encryption at rest on all databases (RDS, S3, etc.). "
"Configure TLS on all services. Use KMS for key management. "
"Document encryption standards in a security policy."
),
status="Implemented",
owner="Engineering",
))
controls.append(build_control_domain(
domain_id="LOG-001",
name="Audit Logging and Monitoring",
description=(
"Comprehensive logging of user activity, system events, and security events. "
"Log integrity protection. SIEM or log aggregation. Alerting on anomalies."
),
soc2_ref="CC7.2, CC7.3",
iso27001_ref="A.8.15, A.8.16, A.8.17",
hipaa_ref="§164.312(b)",
gdpr_ref="Art. 32(1)(b)",
effort_days=20,
cost_usd=30_000, # SIEM tooling
implementation_notes=(
"Centralize logs from application, infrastructure, and cloud provider. "
"Define log retention (minimum 1 year). Set up alerting for authentication "
"failures, privilege escalation, data export events."
),
status="Not Started",
owner="DevOps/Security",
))
controls.append(build_control_domain(
domain_id="IR-001",
name="Incident Response",
description=(
"Documented incident response plan. Defined severity levels. Escalation procedures. "
"Communication templates. Annual tabletop exercise. Post-incident review process."
),
soc2_ref="CC7.3, CC7.4, CC7.5",
iso27001_ref="A.5.24, A.5.25, A.5.26, A.5.27, A.5.28",
hipaa_ref="§164.308(a)(6)",
gdpr_ref="Art. 33, Art. 34",
effort_days=12,
cost_usd=10_000,
implementation_notes=(
"Write IR plan covering detection, containment, eradication, recovery, communication. "
"Define breach notification timelines (GDPR: 72 hours, HIPAA: 60 days). "
"Run annual tabletop exercise. Retain IR firm on retainer."
),
status="In Progress",
owner="CISO",
))
controls.append(build_control_domain(
domain_id="VM-001",
name="Vulnerability Management and Patching",
description=(
"Regular vulnerability scanning of infrastructure and applications. "
"Defined patch SLAs by severity. Penetration testing program. "
"Dependency vulnerability scanning in CI/CD."
),
soc2_ref="CC7.1",
iso27001_ref="A.8.8",
hipaa_ref="§164.308(a)(1)(ii)(A)",
gdpr_ref="Art. 32(1)(d)",
effort_days=15,
cost_usd=20_000,
implementation_notes=(
"Deploy infrastructure scanner (Tenable, Qualys, AWS Inspector). "
"Add SAST/DAST to CI/CD pipeline. Define patch SLAs: Critical <24h, High <7d, "
"Medium <30d. Conduct annual pentest."
),
status="In Progress",
owner="DevOps/Security",
))
controls.append(build_control_domain(
domain_id="VRISK-001",
name="Vendor and Third-Party Risk Management",
description=(
"Inventory of all third-party vendors with data access. Tiered risk assessment "
"process. Contractual security requirements. Annual reviews for critical vendors."
),
soc2_ref="CC9.2",
iso27001_ref="A.5.19, A.5.20, A.5.21, A.5.22",
hipaa_ref="§164.308(b) Business Associate Agreements",
gdpr_ref="Art. 28 Data Processing Agreements",
effort_days=10,
cost_usd=8_000,
implementation_notes=(
"Build vendor inventory spreadsheet. Tier vendors (Tier 1: PII access, "
"Tier 2: business data, Tier 3: no data). Execute DPAs for all processors (GDPR). "
"Execute BAAs for PHI processors (HIPAA). Annual security questionnaire for Tier 1."
),
status="Not Started",
owner="Legal/Security",
))
controls.append(build_control_domain(
domain_id="RISK-001",
name="Risk Assessment and Treatment",
description=(
"Formal risk assessment methodology. Risk register maintained. "
"Risk treatment decisions documented. Annual risk review cycle."
),
soc2_ref="CC3.1, CC3.2, CC3.3, CC3.4",
iso27001_ref="Clause 6.1.2, 6.1.3",
hipaa_ref="§164.308(a)(1) Security Risk Analysis",
gdpr_ref="Art. 32, Art. 35 DPIA",
effort_days=15,
cost_usd=12_000,
implementation_notes=(
"Document risk methodology (FAIR, NIST, ISO 27005). Maintain risk register. "
"HIPAA: formal security risk analysis required — not optional. "
"GDPR: DPIA required for high-risk processing activities. Annual refresh."
),
status="Not Started",
owner="CISO",
))
controls.append(build_control_domain(
domain_id="TRAIN-001",
name="Security Awareness Training",
description=(
"Annual security awareness training for all employees. "
"Role-specific training for high-risk roles. Phishing simulations. "
"Training completion tracking."
),
soc2_ref="CC1.4",
iso27001_ref="A.6.3, A.6.8",
hipaa_ref="§164.308(a)(5)",
gdpr_ref="Art. 39(1)(b)",
effort_days=5,
cost_usd=8_000,
implementation_notes=(
"Deploy security training platform (KnowBe4, Proofpoint, etc.). "
"Annual training required — track completion (100% target). "
"Quarterly phishing simulations. Role-specific training for devs (secure coding), "
"finance (BEC), support (social engineering)."
),
status="Not Started",
owner="HR/Security",
))
controls.append(build_control_domain(
domain_id="CHGMGMT-001",
name="Change Management",
description=(
"Formal change management process for production changes. "
"Code review requirements. Deployment approvals. Rollback procedures. "
"Change log maintained."
),
soc2_ref="CC8.1",
iso27001_ref="A.8.32",
hipaa_ref="§164.312(c)(1) Integrity controls",
gdpr_ref="Art. 25 Privacy by design",
effort_days=10,
cost_usd=5_000,
implementation_notes=(
"Document change management policy. Require peer review for all production changes. "
"Maintain audit trail in version control. No direct production access — "
"all changes via CI/CD pipeline."
),
status="In Progress",
owner="Engineering",
))
controls.append(build_control_domain(
domain_id="BCP-001",
name="Business Continuity and Disaster Recovery",
description=(
"Business continuity plan. Disaster recovery plan with defined RTO/RPO. "
"Backup procedures with tested restores. Failover capabilities."
),
soc2_ref="A1.1, A1.2, A1.3",
iso27001_ref="A.5.29, A.5.30",
hipaa_ref="§164.308(a)(7) Contingency Plan",
gdpr_ref="Art. 32(1)(c)",
effort_days=12,
cost_usd=15_000,
implementation_notes=(
"Define RTO (<4 hours) and RPO (<1 hour) targets. Configure automated backups. "
"Test restore quarterly — paper backups that aren't tested aren't backups. "
"Document DR runbook. Annual DR exercise."
),
status="In Progress",
owner="DevOps",
))
controls.append(build_control_domain(
domain_id="ASSET-001",
name="Asset Inventory and Classification",
description=(
"Complete inventory of hardware, software, and data assets. "
"Data classification scheme. Ownership assigned to all assets. "
"Regular reconciliation."
),
soc2_ref="CC6.1",
iso27001_ref="A.5.9, A.5.10, A.5.11, A.5.12, A.5.13",
hipaa_ref="§164.310(d) Device and Media Controls",
gdpr_ref="Art. 30 Records of Processing Activities",
effort_days=8,
cost_usd=5_000,
implementation_notes=(
"Build asset register (CMDB or spreadsheet at minimum). "
"Classify data: Public, Internal, Confidential, Restricted. "
"GDPR requires RoPA (Record of Processing Activities) — data map of all PII. "
"ISO 27001 requires SoA referencing asset inventory."
),
status="Not Started",
owner="IT/Security",
))
controls.append(build_control_domain(
domain_id="ENDPOINT-001",
name="Endpoint Security",
description=(
"EDR/antivirus on all managed endpoints. Device management (MDM). "
"Full disk encryption. Patch management. BYOD policy."
),
soc2_ref="CC6.8",
iso27001_ref="A.8.1, A.8.7",
hipaa_ref="§164.310(a)(2)(iv) Workstation security",
gdpr_ref="Art. 32(1)(a)",
effort_days=8,
cost_usd=20_000,
implementation_notes=(
"Deploy EDR (CrowdStrike, SentinelOne, or Microsoft Defender for Business). "
"Enable full disk encryption (FileVault/BitLocker). "
"MDM for device management. BYOD policy documented."
),
status="In Progress",
owner="IT",
))
controls.append(build_control_domain(
domain_id="POLICY-001",
name="Security Policies and Procedures",
description=(
"Documented security policies covering acceptable use, access control, "
"incident response, data classification, vendor management, etc. "
"Annual review cycle. Employee attestation."
),
soc2_ref="CC1.2, CC1.3",
iso27001_ref="A.5.1, A.5.2",
hipaa_ref="§164.308(a)(1) Security Management Process",
gdpr_ref="Art. 24 Responsibility of the controller",
effort_days=15,
cost_usd=10_000,
implementation_notes=(
"Minimum policy set: Information Security Policy, Acceptable Use, "
"Access Control, Incident Response, Data Classification, Password, "
"Change Management, Vendor Management, Business Continuity. "
"Use policy templates from GRC platform (Vanta/Drata)."
),
status="In Progress",
owner="CISO",
))
controls.append(build_control_domain(
domain_id="PRIV-001",
name="Privacy and Data Subject Rights",
description=(
"Privacy policy and notices. Data subject rights fulfilment process "
"(access, erasure, portability). Consent management. Cookie compliance. "
"Privacy by design in product development."
),
soc2_ref=None, # Not a SOC 2 requirement (unless Privacy TSC selected)
iso27001_ref="A.5.34",
hipaa_ref="§164.524 Access, §164.528 Accounting of Disclosures",
gdpr_ref="Art. 13, 14, 15–22 (Rights), Art. 25",
effort_days=20,
cost_usd=15_000,
implementation_notes=(
"GDPR: Update privacy policy, implement DSAR process (30-day SLA), "
"build deletion capability into product. Cookie consent (PECR/ePrivacy). "
"HIPAA: Patient rights for PHI access. "
"Consider OneTrust, Termly, or CookieYes for consent management."
),
status="Not Started",
owner="Legal/Product",
))
controls.append(build_control_domain(
domain_id="NET-001",
name="Network Security and Segmentation",
description=(
"Network segmentation (production vs. development vs. corporate). "
"Firewall rules. Intrusion detection. VPN or ZTNA for remote access."
),
soc2_ref="CC6.6, CC6.7",
iso27001_ref="A.8.20, A.8.21, A.8.22",
hipaa_ref="§164.312(e)(1) Transmission security",
gdpr_ref="Art. 32(1)(a)",
effort_days=12,
cost_usd=18_000,
implementation_notes=(
"Segment production from development. WAF in front of public applications. "
"Replace VPN with ZTNA for remote access (Series B+ consideration). "
"DDoS protection (Cloudflare or AWS Shield)."
),
status="In Progress",
owner="DevOps",
))
controls.append(build_control_domain(
domain_id="PENTEST-001",
name="Penetration Testing",
description=(
"Annual external penetration test by qualified third-party firm. "
"Finding remediation tracking. Results reviewed by leadership."
),
soc2_ref="CC7.1",
iso27001_ref="A.8.8",
hipaa_ref="§164.308(a)(8) Evaluation",
gdpr_ref="Art. 32(1)(d)",
effort_days=5,
cost_usd=25_000,
implementation_notes=(
"Scope: external attack surface, application, API, and optionally social engineering. "
"Budget $15–35K for a reputable firm. Track findings in risk register. "
"Re-test critical findings within 90 days. Share pentest summary with enterprise "
"customers on request (under NDA)."
),
status="Not Started",
owner="CISO",
))
return controls
# ─── Analysis ────────────────────────────────────────────────────────────────
def calculate_framework_coverage(controls: list[dict]) -> dict:
"""Calculate per-framework coverage statistics."""
coverage = {}
for fw in FRAMEWORKS:
applicable = [c for c in controls if fw in c["frameworks_applicable"]]
implemented = [c for c in applicable if c["status"] in ("Implemented", "Verified")]
in_progress = [c for c in applicable if c["status"] == "In Progress"]
not_started = [c for c in applicable if c["status"] == "Not Started"]
total_effort = sum(c["effort_days"] for c in applicable)
remaining_effort = sum(
c["effort_days"] for c in applicable
if c["status"] not in ("Implemented", "Verified")
)
total_cost = sum(c["cost_usd"] for c in applicable)
remaining_cost = sum(
c["cost_usd"] for c in applicable
if c["status"] not in ("Implemented", "Verified")
)
pct_complete = (len(implemented) / len(applicable) * 100) if applicable else 0
coverage[fw] = {
"framework": FRAMEWORKS[fw]["name"],
"total_controls": len(applicable),
"implemented": len(implemented),
"in_progress": len(in_progress),
"not_started": len(not_started),
"pct_complete": pct_complete,
"total_effort_days": total_effort,
"remaining_effort_days": remaining_effort,
"total_cost_usd": total_cost,
"remaining_cost_usd": remaining_cost,
"gap_controls": [c["name"] for c in not_started],
}
return coverage
def find_high_leverage_controls(controls: list[dict]) -> list[dict]:
"""Controls that satisfy the most frameworks — highest ROI to implement."""
multi_fw = [c for c in controls if c["framework_count"] >= 3
and c["status"] not in ("Implemented", "Verified")]
return sorted(multi_fw, key=lambda c: (-c["framework_count"], c["effort_days"]))
def estimate_roadmap(controls: list[dict], target_frameworks: list[str]) -> list[dict]:
"""
Generate an ordered implementation roadmap for target frameworks.
Prioritize: (1) controls blocking most frameworks, (2) quick wins (low effort).
"""
applicable = [c for c in controls
if any(fw in c["frameworks_applicable"] for fw in target_frameworks)
and c["status"] not in ("Implemented", "Verified")]
# Score: (frameworks_covered × 10) - (effort_days) → higher is better
for c in applicable:
fw_overlap = len([fw for fw in target_frameworks if fw in c["frameworks_applicable"]])
c["_priority_score"] = (fw_overlap * 10) - c["effort_days"]
return sorted(applicable, key=lambda c: -c["_priority_score"])
def fmt_dollars(amount: float) -> str:
if amount >= 1_000_000:
return f".1fM"
if amount >= 1_000:
return f".0fK"
return f".0f"
def status_icon(status: str) -> str:
icons = {
"Implemented": "✅",
"Verified": "✅",
"In Progress": "🔄",
"Not Started": "⬜",
"Planned": "📋",
}
return icons.get(status, "❓")
# ─── Display ─────────────────────────────────────────────────────────────────
def print_header():
print("\n" + "=" * 80)
print(" CISO COMPLIANCE TRACKER — Multi-Framework Coverage")
print(f" Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 80)
def print_framework_summary(coverage: dict):
print("\n📋 FRAMEWORK COVERAGE SUMMARY")
print("-" * 80)
header = f"{'Framework':<20} {'Done':<6} {'WIP':<5} {'Gap':<5} {'Complete':<10} {'Remain Cost':<14} {'Remain Days'}"
print(header)
print("-" * 80)
for fw_id, data in coverage.items():
pct = f"{data['pct_complete']:.0f}%"
print(
f"{data['framework']:<20} {data['implemented']:<6} {data['in_progress']:<5} "
f"{data['not_started']:<5} {pct:<10} {fmt_dollars(data['remaining_cost_usd']):<14} "
f"{data['remaining_effort_days']} days"
)
def print_control_table(controls: list[dict], framework_filter: Optional[str] = None):
filtered = controls
if framework_filter:
filtered = [c for c in controls if framework_filter in c["frameworks_applicable"]]
title = f"CONTROL DOMAINS"
if framework_filter:
title += f" — {FRAMEWORKS[framework_filter]['name']}"
print(f"\n🔧 {title}")
print("-" * 90)
header = f"{'ID':<14} {'Control Name':<30} {'Frameworks':<8} {'Effort':<8} {'Cost':<10} {'Status'}"
print(header)
print("-" * 90)
for c in filtered:
fw_badges = "/".join(
fw.upper()[:3] for fw in ["soc2", "iso27001", "hipaa", "gdpr"]
if fw in c["frameworks_applicable"]
)
icon = status_icon(c["status"])
print(
f"{c['domain_id']:<14} {c['name'][:29]:<30} {fw_badges:<8} "
f"{c['effort_days']:>3}d {fmt_dollars(c['cost_usd']):<10} {icon} {c['status']}"
)
def print_gap_analysis(coverage: dict):
print("\n⚠️ GAP ANALYSIS — Controls Not Yet Started")
print("-" * 70)
for fw_id, data in coverage.items():
if data["gap_controls"]:
print(f"\n {data['framework']} — {len(data['gap_controls'])} gaps:")
for gap in data["gap_controls"]:
print(f" • {gap}")
def print_high_leverage(controls: list[dict]):
hl = find_high_leverage_controls(controls)
print(f"\n🎯 HIGH-LEVERAGE CONTROLS — Implement Once, Satisfy Multiple Frameworks")
print("-" * 70)
print(f"{'Control':<30} {'Frameworks':<35} {'Effort':<8} {'Cost'}")
print("-" * 70)
for c in hl:
fw_list = " + ".join(FRAMEWORKS[fw]["name"] for fw in c["frameworks_applicable"])
print(
f"{c['name'][:29]:<30} {fw_list[:34]:<35} "
f"{c['effort_days']:>3}d {fmt_dollars(c['cost_usd'])}"
)
def print_roadmap(controls: list[dict], target_frameworks: list[str]):
ordered = estimate_roadmap(controls, target_frameworks)
fw_names = " + ".join(FRAMEWORKS[fw]["name"] for fw in target_frameworks)
print(f"\n🗺️ IMPLEMENTATION ROADMAP — {fw_names}")
print("-" * 80)
print("Priority order: most framework coverage first, then quick wins")
print()
cumulative_days = 0
cumulative_cost = 0
for i, c in enumerate(ordered, 1):
cumulative_days += c["effort_days"]
cumulative_cost += c["cost_usd"]
fw_badges = ", ".join(
FRAMEWORKS[fw]["name"] for fw in target_frameworks
if fw in c["frameworks_applicable"]
)
print(f" {i:>2}. {c['name']}")
print(f" Frameworks: {fw_badges}")
print(f" Effort: {c['effort_days']} days | Cost: {fmt_dollars(c['cost_usd'])} "
f"| Cumulative: {cumulative_days}d / {fmt_dollars(cumulative_cost)}")
if c.get("owner"):
print(f" Owner: {c['owner']}")
print()
def print_framework_profiles():
print("\n💼 FRAMEWORK PROFILES")
print("-" * 70)
for fw_id, fw in FRAMEWORKS.items():
print(f"\n {fw['name']} ({fw_id.upper()})")
print(f" Timeline: ~{fw['typical_timeline_months']} months")
print(f" First-year cost: {fmt_dollars(fw['typical_cost_usd'])}")
print(f" Annual maintenance: {fmt_dollars(fw['annual_maintenance_usd'])}/yr")
print(f" Business value: {fw['business_value']}")
print(f" Required for: {', '.join(fw['mandatory_for'])}")
def export_csv(controls: list[dict], filepath: str):
fields = [
"domain_id", "name", "frameworks_applicable", "framework_count",
"effort_days", "cost_usd", "status", "owner", "target_date",
"soc2_ref", "iso27001_ref", "hipaa_ref", "gdpr_ref", "implementation_notes"
]
with open(filepath, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
for c in controls:
row = {k: c.get(k, "") for k in fields}
row["frameworks_applicable"] = ", ".join(c["frameworks_applicable"])
row["soc2_ref"] = c["references"].get("soc2", "")
row["iso27001_ref"] = c["references"].get("iso27001", "")
row["hipaa_ref"] = c["references"].get("hipaa", "")
row["gdpr_ref"] = c["references"].get("gdpr", "")
writer.writerow(row)
print(f"✅ Exported {len(controls)} controls to {filepath}")
# ─── Main ────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="CISO Compliance Tracker — Multi-framework coverage and roadmap"
)
parser.add_argument("--json", action="store_true", help="Output JSON")
parser.add_argument("--csv", metavar="FILE", help="Export CSV to file")
parser.add_argument(
"--framework", metavar="FRAMEWORK",
choices=list(FRAMEWORKS.keys()),
help="Filter to single framework (soc2, iso27001, hipaa, gdpr)"
)
parser.add_argument("--gap-analysis", action="store_true", help="Show gap analysis")
parser.add_argument("--roadmap", metavar="FRAMEWORKS",
help="Sequenced roadmap for frameworks e.g. 'soc2,iso27001'")
parser.add_argument("--profiles", action="store_true", help="Show framework profiles")
parser.add_argument("--leverage", action="store_true", help="Show high-leverage controls")
args = parser.parse_args()
controls = load_control_library()
coverage = calculate_framework_coverage(controls)
if args.json:
output = {
"generated": datetime.now().isoformat(),
"frameworks": FRAMEWORKS,
"coverage": coverage,
"controls": controls,
}
print(json.dumps(output, indent=2, default=str))
return
if args.csv:
export_csv(controls, args.csv)
return
print_header()
if args.profiles:
print_framework_profiles()
return
if args.roadmap:
target_fws = [fw.strip() for fw in args.roadmap.split(",") if fw.strip() in FRAMEWORKS]
if not target_fws:
print(f"Unknown frameworks. Valid: {', '.join(FRAMEWORKS.keys())}")
sys.exit(1)
print_framework_summary(coverage)
print_roadmap(controls, target_fws)
return
print_framework_summary(coverage)
print_control_table(controls, args.framework)
if args.gap_analysis:
print_gap_analysis(coverage)
if args.leverage:
print_high_leverage(controls)
if not any([args.framework, args.gap_analysis, args.leverage]):
print_high_leverage(controls)
print_gap_analysis(coverage)
print("\n💡 NEXT STEPS")
print(" --roadmap soc2,iso27001 Priority order for dual-framework")
print(" --framework hipaa HIPAA-only control view")
print(" --gap-analysis What's not started")
print(" --leverage Controls covering most frameworks")
print(" --profiles Framework timelines and costs")
print(" --csv controls.csv Export for stakeholder review")
print()
if __name__ == "__main__":
main()
FILE:ciso-advisor/scripts/risk_quantifier.py
#!/usr/bin/env python3
"""
CISO Risk Quantifier
====================
Quantifies security risks in business terms using the FAIR model.
Calculates ALE (Annual Loss Expectancy) and prioritizes by expected annual loss.
Usage:
python risk_quantifier.py # Run with sample data
python risk_quantifier.py --json # Output JSON
python risk_quantifier.py --csv output.csv # Export CSV
python risk_quantifier.py --budget 500000 # Show what fits in budget
python risk_quantifier.py --add # Interactive risk entry
"""
import json
import csv
import sys
import os
import argparse
from datetime import datetime
from typing import Optional
# ─── Data Model ─────────────────────────────────────────────────────────────
RISK_CATEGORIES = [
"Data Breach",
"Ransomware / Extortion",
"Insider Threat",
"Third-Party / Supply Chain",
"Application Vulnerability",
"Cloud Misconfiguration",
"Social Engineering",
"Physical Security",
"Business Email Compromise",
"DDoS / Availability",
]
BUSINESS_IMPACT_TYPES = [
"Revenue Loss",
"Regulatory Fine",
"Legal / Litigation",
"Reputational Damage",
"Recovery / Remediation Cost",
"Customer Churn",
"Business Interruption",
]
MITIGATION_STATUSES = ["None", "Planned", "In Progress", "Mitigated", "Accepted"]
def build_risk(
name: str,
category: str,
description: str,
asset_value: float,
exposure_factor: float, # 0.0–1.0: fraction of asset value lost in breach
annual_rate: float, # ARO: expected incidents per year (0.01 = once per 100 years)
mitigation_cost: float,
mitigation_effectiveness: float, # 0.0–1.0: fraction of risk reduced by control
mitigation_status: str,
business_impacts: dict, # {impact_type: dollar_amount}
notes: str = "",
) -> dict:
"""Construct a risk record with calculated metrics."""
sle = asset_value * exposure_factor # Single Loss Expectancy
ale = sle * annual_rate # Annual Loss Expectancy (inherent)
mitigated_ale = ale * (1 - mitigation_effectiveness) # Residual after mitigation
mitigation_roi = ((ale - mitigated_ale - mitigation_cost) / mitigation_cost * 100
if mitigation_cost > 0 else 0)
total_business_impact = sum(business_impacts.values())
return {
"name": name,
"category": category,
"description": description,
"asset_value": asset_value,
"exposure_factor": exposure_factor,
"annual_rate": annual_rate,
"mitigation_cost": mitigation_cost,
"mitigation_effectiveness": mitigation_effectiveness,
"mitigation_status": mitigation_status,
"business_impacts": business_impacts,
"notes": notes,
# Calculated
"sle": sle,
"ale": ale,
"mitigated_ale": mitigated_ale,
"mitigation_roi_pct": mitigation_roi,
"total_business_impact": total_business_impact,
"priority_score": ale, # Primary sort key
}
# ─── Sample Data ─────────────────────────────────────────────────────────────
def load_sample_risks() -> list[dict]:
"""
Sample risk register for a Series B SaaS company with ~$15M ARR,
~50K customer records, B2B enterprise focus.
"""
risks = []
risks.append(build_risk(
name="Customer Database Breach",
category="Data Breach",
description=(
"Unauthorized access to production database containing 50K+ customer records "
"including PII (name, email, company, payment method). Attack vector: SQL injection, "
"compromised credentials, or insider access."
),
asset_value=5_000_000, # Value of customer database (revenue impact + regulatory)
exposure_factor=0.30, # ~30% of asset value lost in a breach event
annual_rate=0.12, # ~12% chance per year (based on Verizon DBIR industry data)
mitigation_cost=45_000, # WAF + DAST + DB activity monitoring annual cost
mitigation_effectiveness=0.80,
mitigation_status="In Progress",
business_impacts={
"Regulatory Fine": 85_000, # GDPR/CCPA exposure
"Legal / Litigation": 150_000, # Class action exposure
"Customer Churn": 300_000, # Lost ARR from breach-triggered churn
"Reputational Damage": 200_000, # Brand impact / deal loss
"Recovery / Remediation Cost": 65_000,
},
notes="SOC 2 Type II controls partially address. Next step: DB activity monitoring.",
))
risks.append(build_risk(
name="Ransomware Attack",
category="Ransomware / Extortion",
description=(
"Ransomware encrypts production systems. Average ransom demand for a "
"Series B company is $350K–$800K. Recovery without ransom payment: 2–6 weeks downtime. "
"Attack vector: phishing email with malicious attachment, RDP exposure."
),
asset_value=3_500_000,
exposure_factor=0.25,
annual_rate=0.15,
mitigation_cost=60_000, # EDR + email security + backup hardening
mitigation_effectiveness=0.85,
mitigation_status="Planned",
business_impacts={
"Business Interruption": 450_000, # 4 weeks downtime × $112K/week revenue
"Recovery / Remediation Cost": 180_000,
"Customer Churn": 125_000,
"Revenue Loss": 75_000,
},
notes="Offline, tested backups reduce recovery time and eliminate ransom pressure.",
))
risks.append(build_risk(
name="Privileged Insider Data Theft",
category="Insider Threat",
description=(
"Disgruntled or financially motivated employee with elevated access exfiltrates "
"customer data, IP, or trade secrets. Detection is typically slow (median: 197 days "
"per IBM Cost of Data Breach Report)."
),
asset_value=2_800_000,
exposure_factor=0.20,
annual_rate=0.08,
mitigation_cost=35_000, # DLP + UEBA + PAM
mitigation_effectiveness=0.65,
mitigation_status="None",
business_impacts={
"Legal / Litigation": 120_000,
"Customer Churn": 90_000,
"Reputational Damage": 75_000,
"Recovery / Remediation Cost": 40_000,
},
notes="No DLP or UEBA currently deployed. Highest detection gap.",
))
risks.append(build_risk(
name="Critical SaaS Vendor Breach (Supply Chain)",
category="Third-Party / Supply Chain",
description=(
"A critical SaaS vendor (e.g., Salesforce, Slack, AWS, GitHub) suffers a breach "
"that compromises data entrusted to them or disrupts your operations. You have "
"limited control but full liability to customers."
),
asset_value=2_200_000,
exposure_factor=0.15,
annual_rate=0.18,
mitigation_cost=20_000, # Vendor risk assessment program
mitigation_effectiveness=0.40, # Limited — you can't control vendor security
mitigation_status="Planned",
business_impacts={
"Business Interruption": 95_000,
"Customer Churn": 75_000,
"Reputational Damage": 50_000,
"Recovery / Remediation Cost": 30_000,
},
notes="Third-party risk is partially transferable via contractual SLAs and cyber insurance.",
))
risks.append(build_risk(
name="Business Email Compromise (BEC)",
category="Business Email Compromise",
description=(
"Attacker impersonates CEO, CFO, or vendor to redirect wire transfers, gift card "
"purchases, or payroll. Median BEC loss: $125K. FBI IC3 reports BEC as #1 "
"cybercrime by financial loss."
),
asset_value=500_000,
exposure_factor=0.40,
annual_rate=0.30,
mitigation_cost=12_000, # Email authentication (DMARC) + training + callback procedures
mitigation_effectiveness=0.90,
mitigation_status="In Progress",
business_impacts={
"Revenue Loss": 125_000, # Direct financial theft (often unrecoverable)
"Recovery / Remediation Cost": 25_000,
"Legal / Litigation": 15_000,
},
notes="DMARC deployed. Need to enforce wire transfer callback procedures.",
))
risks.append(build_risk(
name="Cloud Misconfiguration — S3 / Storage Exposure",
category="Cloud Misconfiguration",
description=(
"Public exposure of S3 buckets, GCS buckets, or Azure Blob storage containing "
"sensitive data. One of the most common causes of data breaches. Often undetected "
"for months. 2023 IBM study: 82% of breaches involved data stored in cloud."
),
asset_value=1_800_000,
exposure_factor=0.20,
annual_rate=0.20,
mitigation_cost=18_000, # CSPM tool + IaC scanning
mitigation_effectiveness=0.90,
mitigation_status="Planned",
business_impacts={
"Regulatory Fine": 60_000,
"Reputational Damage": 120_000,
"Legal / Litigation": 45_000,
"Recovery / Remediation Cost": 35_000,
},
notes="No CSPM currently. High frequency, high detectability, low mitigation cost.",
))
risks.append(build_risk(
name="Credential Stuffing — Customer Accounts",
category="Application Vulnerability",
description=(
"Attackers use leaked credential lists to compromise customer accounts. "
"Account takeover leads to data theft, fraudulent transactions, and support burden. "
"16 billion credentials available on darknet as of 2024."
),
asset_value=1_200_000,
exposure_factor=0.12,
annual_rate=0.40,
mitigation_cost=15_000, # MFA + rate limiting + bot detection
mitigation_effectiveness=0.95,
mitigation_status="In Progress",
business_impacts={
"Customer Churn": 80_000,
"Revenue Loss": 45_000,
"Recovery / Remediation Cost": 19_000,
"Reputational Damage": 30_000,
},
notes="MFA available but optional. Enforcing MFA cuts this risk by ~99%.",
))
risks.append(build_risk(
name="Phishing — Employee Credential Compromise",
category="Social Engineering",
description=(
"Employee clicks phishing link, surrenders credentials. Without MFA, "
"this provides full access to email, SaaS apps, and potentially production. "
"Phishing is the #1 attack vector in the Verizon DBIR."
),
asset_value=1_500_000,
exposure_factor=0.15,
annual_rate=0.35,
mitigation_cost=25_000, # MFA + security awareness training + email security
mitigation_effectiveness=0.92,
mitigation_status="In Progress",
business_impacts={
"Business Interruption": 65_000,
"Customer Churn": 55_000,
"Recovery / Remediation Cost": 45_000,
"Reputational Damage": 60_000,
},
notes="Primary vector for ransomware and BEC. MFA is the single highest-ROI control.",
))
risks.append(build_risk(
name="Application API Vulnerability",
category="Application Vulnerability",
description=(
"Unauthenticated or improperly authorized API endpoint exposes customer data "
"or administrative functions. OWASP API Security Top 10 — broken object-level "
"authorization is the most common API vulnerability."
),
asset_value=2_000_000,
exposure_factor=0.18,
annual_rate=0.15,
mitigation_cost=30_000, # DAST + API gateway + code review
mitigation_effectiveness=0.75,
mitigation_status="Planned",
business_impacts={
"Regulatory Fine": 70_000,
"Customer Churn": 90_000,
"Reputational Damage": 100_000,
"Legal / Litigation": 60_000,
},
notes="Need automated API security testing in CI/CD pipeline.",
))
risks.append(build_risk(
name="DDoS Attack — Production Service",
category="DDoS / Availability",
description=(
"Distributed denial-of-service attack renders production service unavailable. "
"Average DDoS duration: 4–8 hours. Enterprise SLA breach triggers contractual "
"penalties. Increasingly used as extortion or distraction tactic."
),
asset_value=1_000_000,
exposure_factor=0.10,
annual_rate=0.25,
mitigation_cost=15_000, # CDN with DDoS protection (Cloudflare, AWS Shield)
mitigation_effectiveness=0.85,
mitigation_status="Mitigated",
business_impacts={
"Business Interruption": 45_000,
"Customer Churn": 30_000,
"Revenue Loss": 25_000,
},
notes="Cloudflare deployed. Residual risk from very large volumetric attacks.",
))
return risks
# ─── Analysis & Reporting ────────────────────────────────────────────────────
def calculate_portfolio_summary(risks: list[dict]) -> dict:
"""Aggregate portfolio-level metrics."""
total_inherent_ale = sum(r["ale"] for r in risks)
total_mitigated_ale = sum(r["mitigated_ale"] for r in risks)
total_mitigation_cost = sum(r["mitigation_cost"] for r in risks)
risk_reduction = total_inherent_ale - total_mitigated_ale
portfolio_roi = ((risk_reduction - total_mitigation_cost) / total_mitigation_cost * 100
if total_mitigation_cost > 0 else 0)
by_category = {}
for r in risks:
cat = r["category"]
if cat not in by_category:
by_category[cat] = {"count": 0, "total_ale": 0.0}
by_category[cat]["count"] += 1
by_category[cat]["total_ale"] += r["ale"]
by_status = {}
for r in risks:
status = r["mitigation_status"]
by_status[status] = by_status.get(status, 0) + 1
return {
"total_risks": len(risks),
"total_inherent_ale": total_inherent_ale,
"total_mitigated_ale": total_mitigated_ale,
"total_risk_reduction": risk_reduction,
"total_mitigation_cost": total_mitigation_cost,
"portfolio_roi_pct": portfolio_roi,
"by_category": dict(sorted(by_category.items(), key=lambda x: -x[1]["total_ale"])),
"by_mitigation_status": by_status,
}
def prioritize_risks(risks: list[dict], budget: Optional[float] = None) -> list[dict]:
"""Return risks sorted by ALE. If budget given, show what fits."""
sorted_risks = sorted(risks, key=lambda r: -r["ale"])
if budget is None:
return sorted_risks
# Greedy budget allocation by ROI
actionable = [r for r in sorted_risks if r["mitigation_status"] in ("None", "Planned")
and r["mitigation_cost"] > 0]
actionable.sort(key=lambda r: -r["mitigation_roi_pct"])
allocated = []
remaining = budget
for risk in actionable:
if risk["mitigation_cost"] <= remaining:
allocated.append(risk)
remaining -= risk["mitigation_cost"]
return allocated
def fmt_dollars(amount: float) -> str:
"""Format a dollar amount."""
if amount >= 1_000_000:
return f".2fM"
if amount >= 1_000:
return f".0fK"
return f".0f"
def fmt_pct(value: float) -> str:
return f"{value:.1f}%"
def severity_label(ale: float) -> str:
if ale >= 200_000:
return "CRITICAL"
if ale >= 75_000:
return "HIGH"
if ale >= 25_000:
return "MEDIUM"
return "LOW"
def severity_color(label: str) -> str:
"""ANSI color codes."""
colors = {
"CRITICAL": "\033[91m", # Red
"HIGH": "\033[93m", # Yellow
"MEDIUM": "\033[94m", # Blue
"LOW": "\033[92m", # Green
}
return colors.get(label, "") + label + "\033[0m"
# ─── Display ─────────────────────────────────────────────────────────────────
def print_header():
print("\n" + "=" * 80)
print(" CISO RISK QUANTIFIER — Security Risk Portfolio")
print(f" Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 80)
def print_portfolio_summary(summary: dict):
print("\n📊 PORTFOLIO SUMMARY")
print("-" * 60)
print(f" Total risks tracked: {summary['total_risks']}")
print(f" Total inherent ALE: {fmt_dollars(summary['total_inherent_ale'])}/yr")
print(f" Total ALE after mitigations: {fmt_dollars(summary['total_mitigated_ale'])}/yr")
print(f" Risk reduction from controls: {fmt_dollars(summary['total_risk_reduction'])}/yr")
print(f" Total mitigation spend: {fmt_dollars(summary['total_mitigation_cost'])}/yr")
print(f" Portfolio ROI: {fmt_pct(summary['portfolio_roi_pct'])}")
print()
print(" Risk by Category (sorted by ALE):")
for cat, data in summary["by_category"].items():
print(f" {cat:<35} {data['count']} risks ALE: {fmt_dollars(data['total_ale'])}/yr")
print()
print(" Mitigation Status:")
for status, count in summary["by_mitigation_status"].items():
print(f" {status:<20} {count} risks")
def print_risk_table(risks: list[dict], title: str = "RISK REGISTER"):
print(f"\n🎯 {title}")
print("-" * 80)
header = f"{'#':<3} {'Risk Name':<35} {'Severity':<10} {'ALE/yr':<12} {'Mitig Cost':<12} {'ROI':<8} {'Status':<12}"
print(header)
print("-" * 80)
for i, risk in enumerate(risks, 1):
sev = severity_label(risk["ale"])
sev_str = sev.ljust(10)
roi = fmt_pct(risk["mitigation_roi_pct"]) if risk["mitigation_cost"] > 0 else "N/A"
print(
f"{i:<3} {risk['name'][:34]:<35} {sev_str} "
f"{fmt_dollars(risk['ale']):<12} {fmt_dollars(risk['mitigation_cost']):<12} "
f"{roi:<8} {risk['mitigation_status']}"
)
def print_risk_detail(risk: dict, index: int):
sev = severity_label(risk["ale"])
print(f"\n{'─' * 70}")
print(f" #{index} — {risk['name']} [{sev}]")
print(f"{'─' * 70}")
print(f" Category: {risk['category']}")
print(f" Description: {risk['description'][:120]}...")
print()
print(f" RISK CALCULATION:")
print(f" Asset Value: {fmt_dollars(risk['asset_value'])}")
print(f" Exposure Factor: {fmt_pct(risk['exposure_factor'] * 100)}")
print(f" Single Loss Expectancy: {fmt_dollars(risk['sle'])}")
print(f" Annual Rate (ARO): {risk['annual_rate']:.2f}x/year")
print(f" Annual Loss Expectancy: {fmt_dollars(risk['ale'])}/yr ← INHERENT RISK")
print()
print(f" MITIGATION:")
print(f" Mitigation Cost: {fmt_dollars(risk['mitigation_cost'])}/yr")
print(f" Effectiveness: {fmt_pct(risk['mitigation_effectiveness'] * 100)}")
print(f" Residual ALE: {fmt_dollars(risk['mitigated_ale'])}/yr")
print(f" Mitigation ROI: {fmt_pct(risk['mitigation_roi_pct'])}")
print(f" Status: {risk['mitigation_status']}")
print()
print(f" BUSINESS IMPACT BREAKDOWN:")
for impact_type, amount in risk["business_impacts"].items():
print(f" {impact_type:<30} {fmt_dollars(amount)}")
print(f" {'TOTAL':<30} {fmt_dollars(risk['total_business_impact'])}")
if risk["notes"]:
print(f"\n NOTES: {risk['notes']}")
def print_board_summary(risks: list[dict], summary: dict):
"""One-page board-ready summary."""
print("\n" + "═" * 80)
print(" BOARD SECURITY REPORT — Risk Summary")
print("═" * 80)
critical = [r for r in risks if severity_label(r["ale"]) == "CRITICAL"]
high = [r for r in risks if severity_label(r["ale"]) == "HIGH"]
medium = [r for r in risks if severity_label(r["ale"]) == "MEDIUM"]
low = [r for r in risks if severity_label(r["ale"]) == "LOW"]
print(f"\n RISK EXPOSURE SUMMARY")
print(f" ┌─────────────┬────────┬──────────────┐")
print(f" │ Severity │ Count │ Total ALE/yr │")
print(f" ├─────────────┼────────┼──────────────┤")
for label, group in [("Critical", critical), ("High", high), ("Medium", medium), ("Low", low)]:
ale = sum(r["ale"] for r in group)
print(f" │ {label:<11} │ {len(group):<6} │ {fmt_dollars(ale):<12} │")
print(f" └─────────────┴────────┴──────────────┘")
print(f"\n TOTAL INHERENT RISK: {fmt_dollars(summary['total_inherent_ale'])}/yr")
print(f" SECURITY INVESTMENT: {fmt_dollars(summary['total_mitigation_cost'])}/yr")
print(f" RESIDUAL RISK: {fmt_dollars(summary['total_mitigated_ale'])}/yr")
print(f" RISK REDUCTION: {fmt_dollars(summary['total_risk_reduction'])}/yr")
print(f" PORTFOLIO ROI: {fmt_pct(summary['portfolio_roi_pct'])}")
print(f"\n TOP 3 RISKS BY EXPECTED ANNUAL LOSS:")
top3 = sorted(risks, key=lambda r: -r["ale"])[:3]
for i, risk in enumerate(top3, 1):
print(f" {i}. {risk['name']}: {fmt_dollars(risk['ale'])}/yr expected annual loss")
print(f" Mitigation: {fmt_dollars(risk['mitigation_cost'])}/yr | "
f"Status: {risk['mitigation_status']}")
unmitigated = [r for r in risks if r["mitigation_status"] == "None"]
if unmitigated:
print(f"\n ⚠️ UNMITIGATED RISKS ({len(unmitigated)}):")
for r in sorted(unmitigated, key=lambda x: -x["ale"]):
print(f" • {r['name']}: {fmt_dollars(r['ale'])}/yr — Action required")
def export_csv(risks: list[dict], filepath: str):
fields = [
"name", "category", "asset_value", "exposure_factor", "annual_rate",
"sle", "ale", "mitigation_cost", "mitigation_effectiveness",
"mitigated_ale", "mitigation_roi_pct", "mitigation_status", "notes"
]
with open(filepath, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
for risk in risks:
row = {k: risk.get(k, "") for k in fields}
writer.writerow(row)
print(f"✅ Exported {len(risks)} risks to {filepath}")
def export_json(risks: list[dict]) -> str:
return json.dumps(risks, indent=2, default=str)
# ─── Interactive Entry ───────────────────────────────────────────────────────
def interactive_add_risk() -> dict:
"""Interactive CLI for adding a new risk."""
print("\n── ADD NEW RISK ──────────────────────────────────────")
name = input("Risk name: ").strip()
print(f"Category options: {', '.join(RISK_CATEGORIES)}")
category = input("Category: ").strip()
description = input("Description (brief): ").strip()
print("\nAsset valuation:")
asset_value = float(input(" Asset value ($): ").replace(",", "").replace("$", ""))
exposure_factor = float(input(" Exposure factor (0.0–1.0, fraction of value lost): "))
annual_rate = float(input(" Annual rate of occurrence (e.g., 0.10 = once per 10 years): "))
print("\nMitigation:")
mitigation_cost = float(input(" Mitigation cost ($/yr): ").replace(",", "").replace("$", ""))
mitigation_effectiveness = float(input(" Mitigation effectiveness (0.0–1.0): "))
print(f"Status options: {', '.join(MITIGATION_STATUSES)}")
mitigation_status = input(" Status: ").strip()
print("\nBusiness impacts (enter 0 to skip):")
business_impacts = {}
for impact_type in BUSINESS_IMPACT_TYPES:
val = input(f" {impact_type} ($): ").replace(",", "").replace("$", "")
amount = float(val) if val else 0
if amount > 0:
business_impacts[impact_type] = amount
notes = input("\nNotes: ").strip()
return build_risk(
name=name,
category=category,
description=description,
asset_value=asset_value,
exposure_factor=exposure_factor,
annual_rate=annual_rate,
mitigation_cost=mitigation_cost,
mitigation_effectiveness=mitigation_effectiveness,
mitigation_status=mitigation_status,
business_impacts=business_impacts,
notes=notes,
)
# ─── Main ────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="CISO Risk Quantifier — Quantify security risks in business terms"
)
parser.add_argument("--json", action="store_true", help="Output full JSON")
parser.add_argument("--csv", metavar="FILE", help="Export CSV to file")
parser.add_argument("--budget", type=float, metavar="DOLLARS",
help="Show recommended mitigations within budget")
parser.add_argument("--board", action="store_true", help="Show board-ready summary only")
parser.add_argument("--detail", action="store_true", help="Show detailed risk breakdowns")
parser.add_argument("--add", action="store_true", help="Interactively add a risk")
args = parser.parse_args()
risks = load_sample_risks()
if args.add:
new_risk = interactive_add_risk()
risks.append(new_risk)
print(f"\n✅ Added risk: {new_risk['name']} | ALE: {fmt_dollars(new_risk['ale'])}/yr")
# Sort by ALE descending
risks_sorted = sorted(risks, key=lambda r: -r["ale"])
summary = calculate_portfolio_summary(risks_sorted)
if args.json:
output = {
"generated": datetime.now().isoformat(),
"summary": summary,
"risks": risks_sorted,
}
print(json.dumps(output, indent=2, default=str))
return
if args.csv:
export_csv(risks_sorted, args.csv)
return
print_header()
if args.board:
print_board_summary(risks_sorted, summary)
return
print_portfolio_summary(summary)
print_risk_table(risks_sorted)
if args.detail:
for i, risk in enumerate(risks_sorted, 1):
print_risk_detail(risk, i)
if args.budget:
recommended = prioritize_risks(risks_sorted, args.budget)
print(f"\n💰 BUDGET ALLOCATION — ,.0f")
print(f" Recommended mitigations (sorted by ROI):")
if recommended:
for r in recommended:
print(f" • {r['name']}: {fmt_dollars(r['mitigation_cost'])}/yr "
f"| ALE reduction: {fmt_dollars(r['ale'] - r['mitigated_ale'])}/yr "
f"| ROI: {fmt_pct(r['mitigation_roi_pct'])}")
else:
print(" No actionable mitigations fit within budget.")
print_board_summary(risks_sorted, summary)
print("\n💡 NEXT STEPS")
print(" 1. Run `--detail` to see full breakdown of each risk")
print(" 2. Run `--budget 200000` to see what you can mitigate with a given budget")
print(" 3. Run `--board` for a board-ready one-page summary")
print(" 4. Run `--csv risks.csv` to export for stakeholder review")
print(" 5. Run `--add` to interactively add risks to the register")
print()
if __name__ == "__main__":
main()
FILE:cmo-advisor/SKILL.md
---
name: "cmo-advisor"
description: "Marketing leadership for scaling companies. Brand positioning, growth model design, marketing budget allocation, and marketing org design. Use when designing brand strategy, selecting growth models (PLG vs sales-led vs community-led), allocating marketing budgets, building marketing teams, or when user mentions CMO, brand strategy, growth model, CAC, LTV, channel mix, or marketing ROI."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: cmo-leadership
updated: 2026-03-05
python-tools: marketing_budget_modeler.py, growth_model_simulator.py
frameworks: brand-positioning, growth-frameworks, marketing-org
---
# CMO Advisor
Strategic marketing leadership — brand positioning, growth model design, budget allocation, and org design. Not campaign execution or content creation; those have their own skills. This is the engine.
## Keywords
CMO, chief marketing officer, brand strategy, brand positioning, growth model, product-led growth, PLG, sales-led growth, community-led growth, marketing budget, CAC, customer acquisition cost, LTV, lifetime value, channel mix, marketing ROI, pipeline contribution, marketing org, category design, competitive positioning, growth loops, payback period, MQL, pipeline coverage
## Quick Start
```bash
# Model budget allocation across channels, project MQL output by scenario
python scripts/marketing_budget_modeler.py
# Project MRR growth by model, show impact of channel mix shifts
python scripts/growth_model_simulator.py
```
**Reference docs (load when needed):**
- `references/brand_positioning.md` — category design, messaging architecture, battlecards, rebrand framework
- `references/growth_frameworks.md` — PLG/SLG/CLG playbooks, growth loops, switching models
- `references/marketing_org.md` — team structure by stage, hiring sequence, agency vs. in-house
---
## The Four CMO Questions
Every CMO must own answers to these — no one else in the C-suite can:
1. **Who are we for?** — ICP, positioning, category
2. **Why do they choose us?** — Differentiation, messaging, brand
3. **How do they find us?** — Growth model, channel mix, demand gen
4. **Is it working?** — CAC, LTV:CAC, pipeline contribution, payback period
---
## Core Responsibilities (Brief)
**Brand & Positioning** — Define category, build messaging architecture, maintain competitive differentiation. Details → `references/brand_positioning.md`
**Growth Model** — Choose and operate the right acquisition engine: PLG, sales-led, community-led, or hybrid. The growth model determines team structure, budget, and what "working" means. Details → `references/growth_frameworks.md`
**Marketing Budget** — Allocate from revenue target backward: new customers needed → conversion rates by stage → MQLs needed → spend by channel based on CAC. Run `marketing_budget_modeler.py` for scenarios.
**Marketing Org** — Structure follows growth model. Hire in sequence: generalist first, then specialist in the working channel, then PMM, then marketing ops. Details → `references/marketing_org.md`
**Channel Mix** — Audit quarterly: MQLs, cost, CAC, payback, trend. Scale what's improving. Cut what's worsening. Don't optimize a channel that isn't in the strategy.
**Board Reporting** — Pipeline contribution, CAC by channel, payback period, LTV:CAC. Not impressions. Not MQLs in isolation.
---
## Key Diagnostic Questions
Ask these before making any strategic recommendation:
- What's your CAC **by channel** (not blended)?
- What's the payback period on your largest channel?
- What's your LTV:CAC ratio?
- What % of pipeline is marketing-sourced vs. sales-sourced?
- Where do your **best customers** (highest LTV, lowest churn) come from?
- What's your MQL → Opportunity conversion rate? (proxy for lead quality)
- Is this brand work or performance marketing? (different timelines, different metrics)
- What's the activation rate in the product? (PLG signal)
- If a prospect doesn't buy, why not? (win/loss data)
---
## CMO Metrics Dashboard
| Category | Metric | Healthy Target |
|----------|--------|---------------|
| **Pipeline** | Marketing-sourced pipeline % | 50–70% of total |
| **Pipeline** | Pipeline coverage ratio | 3–4x quarterly quota |
| **Pipeline** | MQL → Opportunity rate | > 15% |
| **Efficiency** | Blended CAC payback | < 18 months |
| **Efficiency** | LTV:CAC ratio | > 3:1 |
| **Efficiency** | Marketing % of total S&M spend | 30–50% |
| **Growth** | Brand search volume trend | ↑ QoQ |
| **Growth** | Win rate vs. primary competitor | > 50% |
| **Retention** | NPS (marketing-sourced cohort) | > 40 |
---
## Red Flags
- No defined ICP — "companies with 50-1000 employees" is not an ICP
- Marketing and sales disagree on what an MQL is (this is always a system problem, not a people problem)
- CAC tracked only as a blended number — channel-level CAC is non-negotiable
- Pipeline attribution is self-reported by sales reps, not CRM-timestamped
- CMO can't answer "what's our payback period?" without a 48-hour research project
- Brand work and performance marketing have no shared narrative — they're contradicting each other
- Marketing team is producing content with no documented positioning to anchor it
- Growth model was chosen because a competitor uses it, not because the product/ACV/ICP fits
---
## Integration with Other C-Suite Roles
| When... | CMO works with... | To... |
|---------|-------------------|-------|
| Pricing changes | CFO + CEO | Understand margin impact on positioning and messaging |
| Product launch | CPO + CTO | Define launch tier, GTM motion, messaging |
| Pipeline miss | CFO + CRO | Diagnose: volume problem, quality problem, or velocity problem |
| Category design | CEO | Secure multi-year organizational commitment to the narrative |
| New market entry | CEO + CFO | Validate ICP, budget, localization requirements |
| Sales misalignment | CRO | Align on MQL definition, SLA, and pipeline ownership |
| Hiring plan | CHRO | Define marketing headcount and skill profile by stage |
| Retention insights | CCO | Use expansion and churn data to sharpen ICP and messaging |
| Competitive threat | CEO + CRO | Coordinate battlecards, win/loss, repositioning response |
---
## Resources
- **References:** `references/brand_positioning.md`, `references/growth_frameworks.md`, `references/marketing_org.md`
- **Scripts:** `scripts/marketing_budget_modeler.py`, `scripts/growth_model_simulator.py`
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- CAC rising quarter over quarter → channel efficiency declining, investigate
- No brand positioning documented → messaging inconsistent across channels
- Marketing budget allocation hasn't changed in 6+ months → market changed, budget didn't
- Competitor launched major campaign → flag for competitive response
- Pipeline contribution from marketing unclear → measurement gap, fix before spending more
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Plan our marketing budget" | Channel allocation model with CAC targets per channel |
| "Position us vs competitors" | Positioning map + messaging framework + proof points |
| "Design our growth model" | Growth projection with channel mix scenarios |
| "Build the marketing team" | Hiring plan with sequence, roles, agency vs in-house |
| "Marketing board section" | Pipeline contribution report with channel ROI |
## Reasoning Technique: Recursion of Thought
Draft a marketing strategy, then critique it from the customer's perspective. Refine based on the critique. Repeat until the strategy survives scrutiny.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:cmo-advisor/references/brand_positioning.md
# Brand Positioning Reference
Practical frameworks for defining, communicating, and defending your market position. Not theory — applied tools for CMOs who need to get this right.
---
## 1. Category Design Frameworks
### The Category Design Principle
Every product exists in a category — either one you define or one someone else defined. If you're not designing your category, your competitors are designing it for you, and they'll design it to exclude you.
**Category design is not renaming an existing category.** It's declaring that the existing category no longer solves the problem adequately, and that a new category — which you happen to lead — is required.
### The Three-Act Category Design Narrative
**Act 1: Name the problem**
Identify a problem that's real, growing, and underserved. Not a problem you invented — a problem your best customers articulate before they've heard your pitch.
> "Enterprise software teams are deploying faster than ever, but their security reviews still take 3 weeks — because security was built for a world where deployments happen monthly, not hourly."
**Act 2: Define the new category**
Name the category in terms of the outcome, not the feature. The category name should describe what customers achieve, not what the product does.
> "Continuous security" — not "automated security scanning" or "DevSecOps platform."
**Act 3: Position yourself as the category leader**
You can't just claim leadership — you need proof: customers, analysts, community, content, events. Leadership is built, not declared.
> "Snyk is building the continuous security category. 1.2M developers have adopted Snyk. Gartner lists us as a Cool Vendor in AppSec."
### When Category Design Works
| Condition | Explanation |
|-----------|-------------|
| Market timing | The problem is growing but the existing category is inadequate |
| CEO commitment | Category design is a 3-5 year initiative, not a marketing campaign |
| Analyst alignment | Gartner, Forrester, or G2 need to recognize your category |
| Community | Practitioners adopt the vocabulary before buyers do |
| Content moat | You publish the defining content for the category before competitors |
### Category Design Pitfalls
- **Naming the category after yourself:** "The [Your Company] Category" is not a category. It's a vanity.
- **Categories that don't solve analyst definitions:** If Gartner doesn't have a Magic Quadrant for your category, you're fighting uphill.
- **Jargon without adoption:** If your category name requires a two-paragraph explanation, it won't stick.
- **Starting a category war you can't win:** If an incumbent can copy your category name and launch in 90 days, you don't have a defensible category.
### The Lightning Strike Strategy
Category design requires concentrated, coordinated effort — not slow drip. Execute these simultaneously:
1. **Major piece of research or data** (the "State of X" report)
2. **Category-defining event** (host it, don't just attend)
3. **Analyst briefing** (educate Gartner/Forrester on the category before they define it themselves)
4. **Book or manifesto** (long-form content that becomes the category Bible)
5. **Community formation** (a Slack group, a conference, a certification that practitioners want)
Do all five within a 3-month window. This creates gravity around your category claim.
---
## 2. Messaging Architecture
### The Messaging Hierarchy
Every piece of content — from a tweet to a 60-page whitepaper — should trace back to this hierarchy. When it doesn't, you have messaging drift.
```
Level 1: Brand Promise
"[Company] [verb] [outcome] for [audience]"
→ Doesn't change. This is the north star.
Level 2: Positioning Statement (internal)
For [target customer] who [has this problem],
[Company] is the [market category] that [differentiated capability]
unlike [alternatives], [Company] [proof of differentiation].
Level 3: Value Propositions (3-4 max, one per key outcome)
Each VP: headline (5-8 words) + 2-3 sentence explanation + proof point
Level 4: Proof Points
Data, case studies, certifications, analyst recognition — evidence for each VP
Level 5: Channel Adaptations
Website copy, sales deck, ad copy, email — same hierarchy, different format
```
### Writing a Positioning Statement
The Geoffrey Moore / April Dunford format is still the best framework:
**Template:**
```
For [specific target customer]
who [has this specific, painful problem],
[Company name] is the [market category]
that [key differentiated capability].
Unlike [primary alternatives],
[Company] [proof of differentiation — something measurable or unique].
```
**Bad example (too generic):**
> For B2B companies who want to grow faster, Acme is the marketing platform that helps you get more leads. Unlike other platforms, Acme is easy to use and powerful.
**Good example (specific and falsifiable):**
> For DevOps teams in regulated industries who spend 20% of their sprint cycles on compliance reviews, Acme is the compliance automation platform that embeds regulatory checks directly into the CI/CD pipeline. Unlike manual compliance tools that create a separate review queue, Acme's policy-as-code approach reduces compliance-related cycle time by 60% without slowing deployments.
**Test your positioning statement:**
1. Can a competitor say the exact same thing? (If yes, it's not differentiated)
2. Does it describe what you do or what the customer gets? (Should be the latter)
3. Would your best customer say "yes, that's exactly my problem"? (If not, wrong ICP)
4. Is it falsifiable? (Claims you can't prove are liabilities)
### Value Proposition Development
**Structure for each VP:**
| Element | Description | Example |
|---------|-------------|---------|
| Outcome headline | What changes for the customer (5-8 words) | "Ship features 3x faster" |
| The problem | Why this matters now (1 sentence) | "Compliance reviews block 40% of releases in regulated industries" |
| Our approach | How we solve it differently (1-2 sentences) | "Policy-as-code embeds checks in the pipeline instead of adding a gate at the end" |
| Proof | Evidence this is real (1 sentence + data point) | "Customers reduce compliance cycle time by 60% in the first 90 days" |
**3-VP Architecture is the standard:**
- VP1: Core outcome (what most customers primarily buy for)
- VP2: Secondary benefit (makes the decision easier or stickier)
- VP3: Differentiator (what tips competitive decisions in your favor)
### Proof Point Hierarchy
Not all proof is equal. When you make a claim, match the strength of your proof to the importance of the claim.
| Proof Type | Strength | Best Used For |
|------------|---------|--------------|
| Third-party data (analyst report, research) | Highest | Category claims, market size |
| Customer ROI data with name | High | Value propositions |
| Customer quote with name and company | Medium-high | Specific pain points and outcomes |
| Aggregated customer data ("customers report…") | Medium | Directional claims |
| Internal testing or benchmark | Medium-low | Product capability claims |
| "Designed to…" or "built for…" | Low | Product direction only |
| "We believe…" or "we think…" | Lowest | Vision statements only |
**Proof point development process:**
1. Write the claim you want to make
2. Identify the strongest available proof
3. If proof is weak, either soften the claim or invest in getting better proof
4. Never publish a claim without knowing what happens when a skeptic asks "prove it"
---
## 3. Competitive Positioning Maps
### The Two-Axis Map
Choose two dimensions that:
1. Both matter to your target buyer
2. Create clear differentiation between you and competitors
3. You can credibly defend
**Choosing the axes:**
- Axis 1 should show a dimension where you win and most competitors cluster on the wrong side
- Axis 2 should show a dimension buyers care about deeply (ease, speed, breadth, price, compliance, etc.)
**What to avoid:**
- "Quality" vs. "Price" — too generic, every company claims the top-left
- Dimensions your competitors can match in one release cycle
- Dimensions that only your product team understands, not buyers
### Competitive Analysis Template
For each major competitor:
**Company:** _______________
| Dimension | What They Claim | What Customers Actually Experience | Gap |
|-----------|----------------|-----------------------------------|-----|
| Positioning | | | |
| Primary differentiator | | | |
| Pricing | | | |
| Ideal customer | | | |
| Weakness (win/loss data) | | | |
| What they say about you | | | |
**Sources for competitive intelligence:**
- Win/loss interviews (primary source — nothing beats this)
- G2/Capterra reviews (what customers say publicly)
- Glassdoor (tells you about internal culture and focus)
- LinkedIn job postings (what they're building next)
- Their pricing page changes (what they're competing on)
- Conference talks from their product and sales leaders
### Battlecard Format
One page per competitor. Used by sales, not marketing.
```
COMPETING AGAINST: [Competitor Name]
WHY CUSTOMERS CONSIDER THEM:
(2-3 bullets — be honest about their appeal)
OUR DIFFERENTIATION:
(2-3 bullets — factual, not marketing language)
THE LANDMINE QUESTION:
(One question that exposes their weakness. The answer should make the buyer uncomfortable choosing them.)
Example: "How long does your typical implementation take? And what's your SLA if it runs over?"
OUR PROOF POINTS IN THIS COMPARISON:
- [Customer name] switched from [competitor] after [specific reason], saw [specific result]
- [Data point that directly contradicts competitor's primary claim]
THEIR LIKELY COUNTER-MOVES:
(What will they say about us? How do we respond?)
WHEN TO WALK AWAY:
(If the prospect values X more than Y, we are not the right fit — say so)
```
---
## 4. Brand Voice Development
### What Brand Voice Is (and Isn't)
**Brand voice is NOT:**
- A list of adjectives ("we are professional, innovative, and customer-focused")
- The tone you use in formal communications
- The font and color palette (that's visual identity)
**Brand voice IS:**
- How the company sounds across every written touchpoint
- Consistent enough to be recognizable, flexible enough to be human
- Grounded in what your best customers actually value
### The Voice Attribute Framework
Define 3-4 voice attributes. For each:
1. **What it means** (in one sentence)
2. **What it sounds like** (one example)
3. **What it doesn't mean** (the common mistake that goes wrong)
**Example:**
| Attribute | Means | Sounds like | Doesn't mean |
|-----------|-------|------------|--------------|
| Direct | We say what we mean without hedging | "Your compliance review takes 3 weeks. It shouldn't." | Blunt, rude, or dismissive |
| Expert | We speak from depth, not from trend | "Here's why most security gates fail at scale, and what actually works." | Jargon-heavy or condescending |
| Honest | We acknowledge what we don't do | "We're not the best fit if you need a one-size-fits-all platform." | Self-deprecating or uncertain |
| Human | Real people write for real people | "Deploying on a Friday? Here's what we'd check first." | Casual, unprofessional |
### Voice Consistency Testing
Take a random sample of 10 recent pieces of content:
- Website homepage and pricing page
- 3 blog posts from different authors
- 5 outbound emails from sales
- 3 social posts
- 1 press release
Score each on: Does this sound like us? (1-5)
Average < 3: You have a brand voice problem. The cause is usually no documented guidelines, or guidelines that exist but aren't enforced.
### Voice in Different Contexts
The attribute stays the same. The tone adjusts.
| Context | Tone adjustment | Example of "Direct" |
|---------|----------------|---------------------|
| Homepage | Confident | "Compliance reviews don't have to slow you down." |
| Technical docs | Precise | "Set the policy threshold to 0.95 to enforce mandatory approval." |
| Error messages | Helpful | "That didn't work. Here's the most common reason why, and how to fix it." |
| Support | Empathetic | "That's frustrating. Here's what happened and what we're doing about it." |
| Sales outreach | Respectful | "Most teams in your space have this problem. Worth 20 minutes to explore?" |
---
## 5. Rebrand Decision Framework
### When Rebrands Succeed vs. Fail
**Successful rebrands:**
- Driven by a genuine strategic shift (new category, new ICP, new market)
- Have internal alignment before external launch
- Are accompanied by product and messaging changes — not just visual
- Have a 6-12 month transition plan for existing customers
**Failed rebrands:**
- Driven by internal boredom with the old brand
- Executed as a "refresh" without repositioning the value proposition
- Lack leadership conviction (executives still describe the company in the old terms)
- Launch with a new logo but same product, same messaging, same ICP
### The Rebrand Decision Matrix
Answer each question. More "yes" answers = more likely rebrand is warranted.
| Question | Yes | No |
|----------|-----|-----|
| Has our ICP changed significantly in the last 18 months? | Rebrand | Stay |
| Are we entering a new market where the current brand creates friction? | Rebrand | Stay |
| Does the brand name have negative associations in the market? | Rebrand | Stay |
| Has an acquisition changed our core identity? | Rebrand | Stay |
| Is the current brand actively hurting sales conversations? (evidence required) | Rebrand | Stay |
| Are we bored with the brand? | Stay | — |
| Did leadership change? | Stay | — |
| Are competitors rebranding? | Stay | — |
Score: 3+ "Rebrand" answers with evidence = worth a serious evaluation.
### Rebrand Risk Assessment
**Name change** is the highest-risk rebrand element. Before committing:
- Legal: trademark availability in all target markets
- SEO: 18-24 months to recover domain authority after a domain change
- Customer: existing customers need to update all integrations, contracts, documentation
- Analyst: re-education of Gartner, Forrester, G2 category definitions
- Employee: company identity shift is a culture event, not just an HR task
**Minimum viable rebrand (lower risk):**
1. New positioning and messaging (always worth doing if positioning is wrong)
2. Visual identity refresh (keep the name, update the look)
3. Tagline change (the cheapest, lowest-risk brand change)
**Full rebrand (high risk, sometimes necessary):**
1. New company name and domain
2. New visual identity
3. New positioning and messaging
4. New category narrative
### Rebrand Execution Checklist
**Pre-launch (90 days):**
- [ ] Finalize positioning before finalizing design (in that order)
- [ ] Legal trademark clearance in all target markets
- [ ] Domain secured (with redirects planned)
- [ ] Internal alignment: every leader can describe the new positioning in one sentence
- [ ] Customer comms plan (existing customers, especially enterprise, need advance notice)
- [ ] Analyst briefings scheduled (Gartner, Forrester — brief them before launch)
- [ ] PR plan finalized
**Launch (day 1):**
- [ ] Website flipped
- [ ] Social profiles updated
- [ ] Email signatures updated company-wide
- [ ] Sales deck updated
- [ ] Press release published
- [ ] Existing customers notified (email from CEO or CMO, not marketing automation)
**Post-launch (90 days):**
- [ ] SEO monitoring (watch for ranking drops on key terms)
- [ ] Win rate monitoring (did conversion change?)
- [ ] Employee feedback (are they using the new messaging correctly?)
- [ ] Partner/channel update (resellers, integrations, directories)
- [ ] Analyst follow-up (did they update their reports?)
---
## Quick Reference: Brand Positioning Diagnostic
Use this as an audit against your current positioning:
| Check | Pass | Fail |
|-------|------|------|
| Can every sales rep state the positioning in one sentence without looking it up? | ✓ | Positioning isn't working |
| Is the ICP specific enough to disqualify companies? | ✓ | ICP is too broad |
| Does the homepage lead with customer outcome, not product features? | ✓ | Copy needs rewrite |
| Can you name 3 companies you're NOT a good fit for? | ✓ | Positioning is unfocused |
| Do win/loss interviews confirm the stated differentiator? | ✓ | Differentiator is assumed, not proven |
| Is the category name used by analysts or industry media? | ✓ | Category design needed |
| Does every piece of content trace back to a VP from the hierarchy? | ✓ | Messaging drift — need guidelines |
FILE:cmo-advisor/references/growth_frameworks.md
# Growth Frameworks Reference
Playbooks for PLG, sales-led, community-led, and hybrid growth models. Includes growth loops, funnel design, and guidance on when and how to switch models.
---
## 1. Product-Led Growth (PLG) Playbook
### What PLG Actually Is
PLG means the product is the primary distribution mechanism. Not "we have a free trial." Not "our product is self-serve." PLG means the product creates acquisition, retention, and expansion — and does so at a scale and cost no sales team can match.
**The minimum requirements for PLG to work:**
1. **Fast time-to-value:** Users must get a meaningful outcome within one session (ideally < 30 minutes)
2. **Low friction to start:** No sales call, no implementation project, no credit card required (for top of funnel)
3. **Built-in virality or network effects:** Usage creates exposure or value that draws in other users
4. **Self-serve monetization or expansion path:** Freemium → paid, or individual → team → company
If any of these is missing, you don't have PLG — you have a website with a free trial.
### PLG Funnel: The Four Stages
**Stage 1: Acquisition**
The user discovers and signs up for the product without talking to sales.
Key channels:
- Organic search (SEO targeting jobs-to-be-done searches)
- Product hunt launches
- Referral and invite loops (users share the product with colleagues)
- Developer communities and open-source contributions
Metric: Visitor-to-signup rate
Benchmark: 2-8% for B2B SaaS (varies heavily by product complexity)
**Stage 2: Activation**
The user reaches the "aha moment" — the point where the product delivers its core value for the first time.
Finding the aha moment:
- Look at the behaviors that differentiate users who stay from users who churn in the first 30 days
- The aha moment is not creating an account. It's completing the first outcome.
- For Slack: sending a message in a real channel
- For Dropbox: adding a file from a second device
- For HubSpot: publishing a form that captures a real lead
Metric: Activation rate (% of signups who complete the aha moment action within 7 days)
Benchmark: 25-40% is strong. < 15% means the onboarding is broken.
**Stage 3: Retention**
Users return to the product and build habitual use.
Retention analysis:
- Cohort retention curves (by signup week/month)
- Day 1, Day 7, Day 30, Day 90 retention rates
- Feature adoption by retained vs. churned users (which features predict retention?)
Metric: D30 retention rate (% of users still active 30 days after signup)
Benchmark: > 40% D30 retention is strong for B2B products
**Stage 4: Revenue**
Self-serve conversion from free to paid, or expansion from individual to team.
PQL (Product-Qualified Lead) signals:
- Reached a usage limit (invites, storage, seats)
- Used a premium feature in trial mode
- Team size on the account reached a threshold
- High-frequency usage above a defined threshold
Metric: PQL conversion rate (% of PQLs who convert to paid within 30 days)
Benchmark: 15-30% for well-designed PLG products
### PLG Expansion Model
PLG growth compounds through account expansion:
```
Individual user discovers product
→ Gets value, invites teammates
→ Team adopts product
→ Becomes department-wide
→ Finance/IT gets involved
→ Enterprise contract
```
This is "bottom-up" enterprise: individual adoption precedes company-wide purchase. It's also the most defensible moat — when every engineer in the company uses your product individually, procurement cancellation is very hard.
**Expansion levers:**
- Seat-based pricing (more users = more revenue, aligned with value)
- Usage-based pricing (more usage = more value = more revenue)
- Feature gating (team/enterprise features visible but gated, creating pull to upgrade)
- Admin discovery (usage reports surface to managers who didn't know they had a product champion)
### PLG Diagnostic
| Question | Healthy | Unhealthy |
|----------|---------|-----------|
| Time-to-value | < 30 minutes | > 2 hours |
| Activation rate | > 30% | < 15% |
| D30 retention | > 40% | < 20% |
| PQL conversion | > 15% | < 5% |
| NPS from self-serve users | > 40 | < 20 |
| Viral coefficient | > 0.3 | < 0.1 |
### PLG Team Structure
```
Head of Growth (often VP Product or VP Marketing)
├── Growth PM (owns activation and retention loops in product)
├── Growth Engineer (2-3 engineers dedicated to growth experiments)
├── Data Analyst (experimentation, funnel analysis, cohort reports)
└── Growth Marketer (acquisition, SEO, referral programs)
```
The growth team sits between product and marketing. This is intentional — they own the product loops that drive acquisition and retention.
---
## 2. Sales-Led Growth (SLG) Model
### The SLG System
In SLG, marketing's job is to fill the sales pipeline. Sales converts it. The system only works if marketing and sales agree on definitions, SLAs, and shared metrics.
**The SLG funnel:**
```
Awareness (Impressions, reach, brand search)
↓
Lead (Name + contact info captured)
↓
MQL — Marketing Qualified Lead (meets ICP criteria, intent signal detected)
↓ [Marketing → Sales handoff]
SAL — Sales Accepted Lead (sales reviews and accepts the lead)
↓
SQL — Sales Qualified Lead (sales confirms budget, authority, need, timeline)
↓
Opportunity (Formal deal in pipeline, has a close date)
↓
Closed-Won
```
**The MQL definition problem:**
Most marketing-sales friction traces to an unclear MQL definition. The MQL should be:
- ICP-matched (company size, industry, role)
- Intent-signaled (visited pricing page, attended webinar, downloaded high-intent content)
- Not just email address + "subscribed to newsletter"
**A concrete MQL definition:**
> Company 50-500 employees, B2B SaaS, role is VP Engineering or CTO or CISO, AND has performed 2+ of: attended webinar, visited pricing page, requested demo, downloaded security report, attended event.
This definition makes the MQL useful. If you can't score it in your CRM without human judgment, it's not a definition — it's a guideline.
### SLG Conversion Rate Benchmarks
| Stage | Average B2B SaaS | Top Quartile |
|-------|-----------------|--------------|
| Lead → MQL | 5-15% | > 20% |
| MQL → SAL | 50-70% | > 75% |
| SAL → SQL | 30-50% | > 60% |
| SQL → Opportunity | 60-80% | > 85% |
| Opportunity → Closed-Won | 20-30% | > 40% |
**End-to-end:** Lead → Closed-Won: 1-5% (wide range by ACV and ICP quality)
### Pipeline Coverage Mechanics
A healthy SLG pipeline has 3-4x coverage against quota.
If a sales rep has a $500K quarterly quota:
- They need $1.5M-$2M in active pipeline
- Pipeline must be distributed across stages (not all "prospecting")
- Stage distribution benchmark: 30% early, 40% mid, 30% late
Insufficient coverage (< 3x) is a lagging indicator of a miss — by the time coverage is low, it's too late to recover in the same quarter. Coverage should be tracked weekly.
### SLG Demand Generation Channels
**High-intent channels (bottom of funnel):**
- Paid search on buying-intent keywords (e.g., "[competitor] alternative", "best [category] software")
- Review site presence (G2, Capterra) — buyers use these before vendor websites
- Outbound SDR targeting specific accounts (ABM)
**Medium-intent channels (middle of funnel):**
- Webinars and virtual events (capture active learners)
- Gated content (guides, benchmarks, templates — ICP-specific)
- Retargeting to website visitors
**Awareness channels (top of funnel):**
- Content and SEO (captures people learning about the problem)
- Podcast sponsorships, industry media
- Conference sponsorship and speaking
- Paid social (LinkedIn for B2B)
### ABM (Account-Based Marketing) in SLG
ABM flips the funnel: instead of generating leads and filtering for good ones, you start with target accounts and run coordinated campaigns against them.
**Tiers:**
- **Tier 1 (1:1):** 5-20 strategic accounts, fully customized campaigns, dedicated SDR+AE pairs, executive outreach
- **Tier 2 (1:few):** 50-200 accounts, programmatic personalization, SDR sequences, targeted events
- **Tier 3 (1:many):** 500+ accounts, standard campaigns with light personalization
ABM requires tight sales/marketing alignment. If sales doesn't work the accounts marketing targets, ABM produces zero results.
---
## 3. Community-Led Growth (CLG)
### The CLG Thesis
Community-led growth works when:
1. Your buyers want to learn from peers, not vendors
2. There's a strong practitioner identity (developers, data teams, security, FinOps)
3. Your category is complex enough that buyers need education before purchasing
4. You can commit to building genuine community, not a marketing channel in disguise
**The fundamental rule of CLG:** The community must deliver value to members whether or not they ever buy your product. If the only purpose of the community is to sell to members, the community will die.
### CLG Stages
**Stage 1: Find the community**
The community often exists before you build it. Find where your practitioners already gather:
- Slack groups, Discord servers
- Subreddits and LinkedIn groups
- Conference hallways
- Open-source repositories
Before building, participate. Earn trust. Understand the conversations.
**Stage 2: Become the knowledge hub**
Establish your company as the best source of information on the category problem:
- Publish the benchmark study everyone references
- Host the conference that defines the industry
- Create the certification practitioners want on their resume
- Open-source the tools the community needs
**Stage 3: Build the platform**
Create a dedicated community space (Slack, Discord, forum):
- Community must be practitioner-first, not vendor-first
- Community managers who genuinely care about member value
- Content from members, not just from your company
- Events that build member relationships, not just product demos
**Stage 4: Convert community to customers**
Community members who become customers do so because they trust you, not because you sold them. Conversion paths:
- Community members see peer success with your product
- Product-qualified signals from community members who trial the product
- Direct outreach from sales to active community members (with permission and context)
- Enterprise deals from companies whose employees are active in the community
### CLG Metrics
| Metric | Definition | Health Signal |
|--------|-----------|--------------|
| Monthly active members | Members who post, comment, or engage | > 15% of total members |
| Community-sourced pipeline | $ pipeline where community was first touch | Track and trend |
| Community-influenced pipeline | $ pipeline with any community touchpoint | > 30% of total pipeline |
| NPS of community members vs. non-members | Loyalty difference | Community members should score 20+ pts higher |
| Member-generated content % | % of content posted by non-employees | > 60% is healthy community |
| Time from community join to product trial | | Shortens as community matures |
### CLG Anti-Patterns
- **Community as a newsletter:** If members can't interact with each other, it's not a community — it's a list.
- **Product launches in the community:** Nothing kills community trust faster than using it for sales announcements.
- **Community without a community manager:** Communities left to run themselves become ghost towns or become toxic.
- **Measuring community by member count:** Ghost members are noise. Active engagement is signal.
---
## 4. Hybrid Growth Models
### PLG + SLG ("Product-Led Sales" or PLS)
The most common hybrid at growth stage. PLG handles SMB self-serve; sales closes enterprise.
**The PQL-to-sales handoff:**
Define the triggers that move a product-qualified lead to a sales-assisted motion:
- Company has > X users (e.g., 10+ users on a team account)
- Usage exceeds Y threshold in 30 days
- Account is a named target in the ABM list
- User explicitly requested a demo or upgrade assistance
**The risk:** Sales team ignores PLG pipeline because deal size is smaller. Fix: separate quotas and commission structures for self-serve expansion vs. new enterprise logos.
**The opportunity:** PLG creates pre-qualified champions inside accounts. Sales doesn't have to create interest — they convert it. Win rates in PLS motions are typically 30-50% higher than cold outbound.
### SLG + CLG
Community builds brand and generates inbound pipeline for sales.
This hybrid works when:
- Sales cycles are long (6-18 months)
- Buyers do extensive research before engaging with vendors
- The community validates your credibility before sales conversations begin
**The integration:**
- Community team feeds content insights to demand gen
- Event attendees become high-priority SDR sequences
- Active community members get dedicated AE outreach with community context
- Win/loss analysis includes community touchpoints
### PLG + CLG
The developer/open-source hybrid. PLG handles product adoption; community handles advocacy and content.
**Examples:** HashiCorp (Terraform community + enterprise sales), Elastic (open-source + community + commercial), Tailscale (developer community + self-serve + enterprise).
**How it compounds:**
```
Community member learns from community content
→ Discovers open-source or free tier
→ Gets value in first session
→ Shares experience in community
→ New members discover product through community content
```
---
## 5. Growth Loops vs. Funnels
### The Difference
**A funnel** is linear. It requires constant input at the top to produce output at the bottom. If you stop feeding it, it stops producing.
**A growth loop** is cyclical. Output from one stage becomes input to the next. The system compounds.
### Common Growth Loops
**Viral loop:**
```
User gets value → Invites colleague → Colleague signs up →
Colleague invites another colleague → ...
```
Viral coefficient (K) = (Average invites per user) × (Conversion rate of invites)
- K > 1: Exponential growth (rare)
- K 0.5-1: Strong viral assist
- K < 0.3: Viral is not a meaningful growth driver
**Content SEO loop:**
```
Publish content on [topic] → Ranks in search →
Drives signups → Users share content → Builds backlinks →
Better rankings → More content is possible
```
This loop takes 12-24 months to activate but is extraordinarily defensible once running.
**UGC (User-Generated Content) loop:**
```
Users share their work publicly (templates, analyses, portfolios) →
Others discover the work → They find the product →
They create and share their own work → ...
```
Figma, Notion, Airtable, Canva — all run this loop.
**Data network effect loop:**
```
More users → More data → Better product →
More users attracted → ...
```
LinkedIn, Waze, Duolingo — accuracy or relevance improves as the user base grows.
**Integration loop:**
```
Product integrates with X → X's users discover your product →
More integrations possible → More discovery surfaces → ...
```
Zapier, Slack apps, Salesforce AppExchange — being in the ecosystem creates distribution.
### Building a Growth Loop
**Step 1: Map the current funnel**
Where do customers come from? What are the conversion steps?
**Step 2: Find the output**
What does a successful customer produce?
- Invite emails
- Shared content
- Public work visible to others
- Reviews or testimonials
**Step 3: Design the loop**
How does that output become tomorrow's input to acquisition?
- If they share → is there a landing page that captures the new visitor?
- If they invite → is the invite experience friction-free?
- If they create content → does it rank in search or appear in relevant communities?
**Step 4: Measure loop velocity**
For each loop, measure:
- Cycle time: How long does one full cycle take?
- Conversion at each step: Where does the loop break down?
- Loop coefficient: How many new users does one existing user generate?
---
## 6. When to Switch Growth Models
### The Warning Signs
**PLG-to-SLG triggers:**
- Enterprise accounts are signing up via PLG but aren't expanding without human intervention
- Average deal sizes in enterprise are 10-20x SMB, and you're leaving revenue on the table
- Product adoption in enterprise requires configuration or integration that needs support
- PLG accounts churn at higher rates than sales-assisted accounts
**SLG-to-PLG/PLS triggers:**
- CAC is increasing year-over-year as competition for sales talent intensifies
- Smaller competitors are winning deals with self-serve
- Customers are asking "can I just try this myself?"
- ACV is declining as the market matures and products commoditize
- Sales team efficiency (revenue per sales rep) is declining
**Adding CLG to existing motion:**
- Sales cycles are long and trust is the primary barrier
- SEO and content are generating traffic but low conversion (awareness without trust)
- Competitors are building community and you're not present
- Customer success teams report that customers who participate in user groups retain better
### The Transition Playbook
**Phase 1: Prove it before scaling (months 1-6)**
Don't restructure the team to support the new model before proving it works.
- Run a pilot: 3-5 SDRs testing PLG signals as outreach triggers (for PLG → PLS)
- Or: Launch a beta community with 100 core customers (for adding CLG)
- Measure the metrics of the new model, compare to current model
**Phase 2: Parallel running (months 6-12)**
Run both models simultaneously. Don't kill the current model while building the new one.
- Set clear boundaries on which accounts go to which motion
- Build dedicated teams for each model (don't ask the same people to do both)
- Define success metrics for the new model independently
**Phase 3: Rebalance (months 12-18)**
Once the new model proves its unit economics:
- Shift headcount and budget to the more efficient model
- Keep the old model for the segments where it still works
- Document what the new model requires to sustain itself
**The anti-pattern:** Announcing a model shift without proof, restructuring the team, and discovering after 12 months that the new model doesn't work. By then, the old model's momentum is gone and you've burned a year.
### Growth Model Maturity Matrix
| Dimension | PLG | SLG | CLG |
|-----------|-----|-----|-----|
| Time to first results | 3-6 months | 1-3 months | 12-18 months |
| Requires up-front product investment | High | Low | Medium |
| Scales without linear headcount | Yes | No | Yes |
| Predictable pipeline | Low (early) | High | Low (early) |
| CAC trend over time | Decreases | Flat/increases | Decreases |
| Works for ACV > $50K | Only with SLG assist | Yes | Yes |
| Works for ACV < $5K | Yes | No | Only with PLG |
| Defensibility once established | High | Low | Very high |
FILE:cmo-advisor/references/marketing_org.md
# Marketing Org Reference
Team structure, hiring sequence, agency decisions, marketing ops, and cross-functional alignment — by company stage.
---
## 1. Marketing Team Structure by Stage
### Pre-Seed / Seed (< $1M ARR, 1–10 people)
Don't hire a marketing team yet. The founders are the marketing team.
What to do instead:
- Founders write content, do sales calls, go to events
- The goal is learning the ICP and finding the channel that works, not scaling anything
- One contractor or agency for specific output (design, SEO audit) is fine
First marketing hire trigger: You have a repeatable sales motion and need to scale it.
---
### Series A ($1M–$5M ARR, 10–30 people)
**Org:**
```
Founding Marketer (Head of Marketing or VP Marketing)
```
One person. Generalist. Capable of writing, running ads, setting up HubSpot, producing a report. Their job is to find what works.
**What they own:**
- Content and SEO foundation
- Paid channel experiments
- Sales enablement basics (1-pager, deck, email sequences)
- Event presence (1-2 conferences)
- Marketing attribution setup (get this right early)
**What they don't own yet:**
- Brand redesign
- Analyst relations
- Partner marketing
- Field marketing team
**CMO vs. VP Marketing at this stage:** VP Marketing. An experienced operator who can build and execute. A CMO's strategic value isn't fully leveraged until there's a team to lead and a budget to allocate.
---
### Series B ($5M–$20M ARR, 30–80 people)
**PLG-first org:**
```
VP Marketing
├── Growth Marketing (acquisition loops, activation, PLG analytics)
├── Product Marketing (positioning, launch, sales enablement)
└── Content & SEO (organic engine)
```
**SLG-first org:**
```
VP Marketing
├── Demand Generation (pipeline creation, paid, digital)
├── Product Marketing (positioning, competitive intel, enablement)
├── Field Marketing (events, regional, ABM)
└── Marketing Operations (CRM, attribution, reporting)
```
**Community-led org:**
```
VP Marketing
├── Community & Developer Relations
├── Content & SEO
└── Product Marketing
```
**At this stage:** Marketing ops becomes critical. Without it, attribution is guesswork and the sales team blames marketing for bad leads.
---
### Series C ($20M–$75M ARR, 80–200 people)
```
CMO
├── Demand Generation
│ ├── Paid Media
│ ├── SEO & Content
│ └── Marketing Operations
├── Product Marketing
│ ├── Core PMMs (by product line or segment)
│ └── Competitive Intelligence
├── Field Marketing
│ ├── Events
│ └── Regional / ABM
└── Brand & Communications
├── Brand Design
└── PR / Analyst Relations
```
**At this stage:**
- The CMO is a board-level communicator, not a campaign manager
- Each function has a dedicated leader (director or VP level)
- Marketing ops owns the attribution model and reports to CMO directly
- Analyst relations becomes important (Gartner, Forrester, G2 category positioning)
---
### Growth Stage ($75M+ ARR)
Marketing becomes a portfolio of specialized functions. Each major channel has a team. Brand is a serious investment. Analyst relations is a dedicated role. International marketing teams form.
The CMO's job shifts from building the machine to:
- Setting marketing strategy across a complex portfolio
- Representing marketing at the board level
- Owning brand and category leadership
- Cross-functional leadership with CRO, CPO, CEO
---
## 2. Hiring Sequence
### Who to Hire First
**The generalist content + demand gen marketer.**
Must-haves:
- Can write (blog posts, emails, landing pages — not just briefs)
- Can run paid campaigns (Google, LinkedIn — not just "I've managed agencies")
- Can operate a marketing automation platform (HubSpot, Marketo)
- Comfortable with data (can build a funnel report without asking an analyst)
This person builds the foundation. They're not a specialist yet — they're testing channels and building the process.
Avoid: Hiring a brand designer first. Or a community manager. Or a social media manager. These are specialties that compound on a foundation that doesn't exist yet.
### Who to Hire Second
**A specialist in the channel that's working.**
If organic search is your top lead source → hire an SEO/content lead.
If events are driving pipeline → hire a field marketer.
If outbound is working → hire an SDR manager or demand gen specialist.
Don't hire a generalist #2. By now you know what's working. Depth beats breadth.
### Who to Hire Third
**Product marketing.**
Why third and not first? Because PMM output (positioning, sales enablement, launch) is most valuable when there's an audience to position to and a sales team to enable. Before that, the founding marketer does "good enough" PMM work.
PMM hire profile: Has done positioning work before, has run a product launch, has built sales decks that sales actually uses, comfortable with win/loss analysis.
PMM:PM ratio benchmark: 1 PMM per 2–3 PMs. If you have 6 PMs and 1 PMM, you have a messaging and enablement problem.
### Who to Hire Fourth
**Marketing operations.**
This is consistently hired too late. By the time most companies hire marketing ops, attribution is broken, leads are being lost in handoffs, and the CRM data is unreliable. Hire marketing ops before you think you need it.
Marketing ops profile: HubSpot/Marketo certified, SQL capable, understands multi-touch attribution, has integrated CRM + sales engagement tools before.
### Hiring Decision Triggers
| Hire | Trigger |
|------|---------|
| Generalist marketer #1 | Sales motion is repeatable, need to scale lead generation |
| Specialist #2 | One channel is clearly outperforming — double down |
| Product marketer | Sales team is losing deals to positioning confusion or competitor gaps |
| Marketing ops | Running 3+ campaigns simultaneously with manual tracking |
| Field marketer | Events are in the strategy and attendance > 2 conferences/quarter |
| Head of Marketing / VP | Team is 3+ people and needs an org owner |
| CMO | Company is Series B/C and marketing needs board-level representation |
---
## 3. Agency vs. In-House
### Framework
Keep in-house what compounds. Outsource what's episodic or specialized.
| Function | Agency | In-House | Notes |
|----------|--------|----------|-------|
| Brand design | Early stage | Series B+ | Agency fine until redesigns become frequent |
| Paid media | < $50K/month spend | > $50K/month | Agency margin eats returns at scale |
| SEO strategy | Audit only | Ongoing execution | Strategy once, execution continuously |
| Content production | Overflow only | Core writers | Your voice must be yours |
| PR / comms | Almost always | $100M+ companies | Specialists required for media relationships |
| Marketing ops / CRM | Never | Always | This is your data infrastructure |
| Analyst relations | Initial strategy | Ongoing | Relationship-based — needs dedicated owner |
| Video / creative production | Always | Rarely | Episodic, specialized equipment |
### Agency Red Flags
- They want to own your ad accounts. (Always keep ownership. No exceptions.)
- SLA is "5 business days for creative requests." For a performance channel, that's too slow.
- Reporting is impressions, CPM, and "brand lift." Where's the pipeline?
- They can't tell you your CAC from their channel.
- They won't share the actual data — only their dashboard.
- Your account manager changes every 6 months.
### Agency Evaluation Criteria
1. **Proof of work in your category** — ask for 3 case studies with actual CAC and pipeline data
2. **Who actually does the work** — senior pitch team ≠ junior execution team
3. **Account ownership** — all accounts, pixels, analytics must be in your name
4. **Reporting cadence** — weekly data, monthly strategy, quarterly business review
5. **Exit terms** — how do you offboard without losing your data, accounts, and history?
---
## 4. Marketing Ops and Tech Stack
### The Minimum Viable Stack
| Layer | Tool | Purpose |
|-------|------|---------|
| CRM | HubSpot / Salesforce | Contact database, pipeline, source of truth |
| Marketing automation | HubSpot / Marketo / ActiveCampaign | Email, nurture, lead scoring |
| Analytics | Google Analytics 4 + Segment | Traffic, behavior, event tracking |
| Attribution | HubSpot / Attributer.io / Dreamdata | Multi-touch pipeline attribution |
| Paid | Google Ads + LinkedIn Ads | Performance channels |
| SEO | Ahrefs / Semrush | Keyword research, rank tracking |
| Chat/conversion | Intercom / Drift | In-product + website conversion |
**The integration that breaks most:** CRM ↔ Marketing automation ↔ Sales engagement. When these aren't synced properly, leads are lost, attribution is wrong, and marketing and sales fight about pipeline. Fix this first.
### Marketing Ops Ownership
Marketing ops must own:
- CRM data quality (field standardization, deduplication, routing)
- Lead scoring model (and quarterly review against conversion data)
- Attribution model (with documented assumptions)
- Campaign tracking (UTM governance — no UTM = no attribution)
- Tech stack evaluation and contracts
Marketing ops must NOT own:
- Strategy (they enable it, not set it)
- Content production
- Campaign creative
---
## 5. Cross-Functional Alignment
### Marketing + Sales
The most important cross-functional relationship in a SLG company. Where it breaks:
| Problem | Root Cause | Fix |
|---------|-----------|-----|
| "Marketing sends us bad leads" | MQL definition is unclear or wrong | Define MQL jointly, score against conversion data |
| "Sales doesn't follow up on leads" | No SLA, no consequence | Define SLA (e.g., 24-hour response), track in CRM |
| "Marketing doesn't understand what customers care about" | No win/loss sharing | Weekly call: sales shares 3 deal insights, marketing shares 3 content results |
| "We don't know what's working" | Attribution is broken | Marketing ops fixes attribution before next budget cycle |
**The SLA agreement (document this):**
- Marketing commits: X MQLs/week meeting defined criteria, 48-hour SLA from form fill to SDR outreach
- Sales commits: All MQLs contacted within 24 hours, disposition logged in CRM within 5 days
### Marketing + Product
Where it breaks and how to fix it:
| Problem | Fix |
|---------|-----|
| PMM learns about launches 2 weeks before ship | PMM joins the product planning process at the roadmap stage, not the sprint stage |
| Feature launches with no messaging | Launch tiers: Tier 1 (major, full launch), Tier 2 (minor, release notes + 1 post), Tier 3 (internal only) |
| Product doesn't use customer insights from marketing | Monthly session: PMM shares win/loss themes, competitive intel, ICP data |
| No feedback loop on messaging in-product | PMM owns in-product copy review, not just external comms |
### Marketing + Customer Success
Customer success is marketing's best source of truth:
- **ICP validation:** Which customers are expanding? Which are churning? This refines who you target.
- **Proof points:** CS-sourced case studies and testimonials outperform vendor-written content 3:1 in conversion.
- **Messaging test:** If CS is answering the same question 20 times, marketing hasn't explained it clearly enough.
- **Referral programs:** CS owns the relationship; marketing owns the mechanics. Design them together.
Cadence: Monthly meeting between CMO and VP/Head of CS. Agenda: retention trends, expansion patterns, at-risk customers, NPS themes.
FILE:cmo-advisor/scripts/growth_model_simulator.py
#!/usr/bin/env python3
"""
Growth Model Simulator
----------------------
Projects MRR growth across different growth models (PLG, sales-led, community-led,
hybrid) and shows the impact of channel mix changes on growth trajectory.
Usage:
python growth_model_simulator.py
Inputs (edit INPUTS section):
- Starting MRR and churn rate
- Current channel mix (% of new MRR from each source)
- Conversion rates per model
- Growth rate assumptions per channel
Outputs:
- 12-month MRR projection by growth model
- Channel mix impact analysis (what happens if you shift mix)
- Break-even months for each model
- Side-by-side comparison table
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------
@dataclass
class ChannelSource:
name: str
pct_of_new_mrr: float # Current share of new MRR (0.0–1.0)
monthly_growth_rate: float # How fast this channel grows month-over-month
cac: float # CAC in dollars
payback_months: float # Months to recover CAC
@dataclass
class GrowthModel:
name: str
description: str
channel_mix: Dict[str, float] # channel name → % of new MRR
new_mrr_monthly_base: float # Starting new MRR/month from this model
monthly_acceleration: float # Acceleration factor (compounding)
avg_ltv_cac: float # Expected LTV:CAC at scale
months_to_steady_state: int # Months before model hits its natural growth rate
notes: List[str] = field(default_factory=list)
@dataclass
class MonthSnapshot:
month: int
mrr: float
new_mrr: float
churned_mrr: float
expansion_mrr: float
net_new_mrr: float
cumulative_cac_spend: float
@dataclass
class ModelProjection:
model: GrowthModel
snapshots: List[MonthSnapshot]
break_even_month: Optional[int] # Month when cumulative revenue > cumulative CAC
# ---------------------------------------------------------------------------
# INPUTS — edit these
# ---------------------------------------------------------------------------
STARTING_MRR = 85_000 # Current MRR ($)
MONTHLY_CHURN_RATE = 0.012 # Monthly churn rate (1.2% = ~14% annual)
EXPANSION_RATE = 0.008 # Monthly expansion MRR as % of existing MRR
GROSS_MARGIN = 0.75
SIMULATION_MONTHS = 18
# Channel sources (used to model mix shift scenarios)
CHANNELS: List[ChannelSource] = [
ChannelSource("Organic/SEO", pct_of_new_mrr=0.28, monthly_growth_rate=0.04, cac=1_800, payback_months=9),
ChannelSource("PLG Self-Serve", pct_of_new_mrr=0.15, monthly_growth_rate=0.08, cac=900, payback_months=5),
ChannelSource("Outbound SDR", pct_of_new_mrr=0.25, monthly_growth_rate=0.02, cac=5_100, payback_months=21),
ChannelSource("Paid Search", pct_of_new_mrr=0.15, monthly_growth_rate=0.01, cac=6_200, payback_months=26),
ChannelSource("Events/Field", pct_of_new_mrr=0.08, monthly_growth_rate=0.01, cac=9_800, payback_months=41),
ChannelSource("Partner/Channel", pct_of_new_mrr=0.09, monthly_growth_rate=0.05, cac=3_400, payback_months=14),
]
# Growth models to simulate
GROWTH_MODELS: List[GrowthModel] = [
GrowthModel(
name="Current Mix",
description="Baseline — maintain current channel allocation",
channel_mix={"Organic/SEO": 0.28, "PLG Self-Serve": 0.15, "Outbound SDR": 0.25,
"Paid Search": 0.15, "Events/Field": 0.08, "Partner/Channel": 0.09},
new_mrr_monthly_base=12_000,
monthly_acceleration=0.025,
avg_ltv_cac=3.2,
months_to_steady_state=3,
notes=["Baseline. No changes to channel mix."],
),
GrowthModel(
name="PLG-First",
description="Shift budget toward PLG self-serve and organic; reduce paid and outbound",
channel_mix={"Organic/SEO": 0.35, "PLG Self-Serve": 0.35, "Outbound SDR": 0.10,
"Paid Search": 0.08, "Events/Field": 0.04, "Partner/Channel": 0.08},
new_mrr_monthly_base=9_500, # Slower start — PLG takes time to activate
monthly_acceleration=0.048, # But compounds faster
avg_ltv_cac=5.8,
months_to_steady_state=6, # PLG loops take time to build
notes=[
"Lower new MRR in months 1-6 while PLG loops activate.",
"Acceleration compounds strongly after month 6.",
"Requires product investment in activation/onboarding.",
"Best fit if time-to-value < 30 min and viral coefficient > 0.3.",
],
),
GrowthModel(
name="Sales-Led Scale",
description="Double down on outbound SDR and field; optimize for enterprise ACV",
channel_mix={"Organic/SEO": 0.20, "PLG Self-Serve": 0.05, "Outbound SDR": 0.40,
"Paid Search": 0.15, "Events/Field": 0.15, "Partner/Channel": 0.05},
new_mrr_monthly_base=15_000, # Higher new MRR from enterprise ACV
monthly_acceleration=0.018, # Linear growth — headcount-constrained
avg_ltv_cac=2.8,
months_to_steady_state=2,
notes=[
"Fastest short-term new MRR if ACV > $30K.",
"Growth is linear — adds headcount to add pipeline.",
"CAC and payback worsen as SDR market tightens.",
"Requires sales capacity increase to sustain.",
],
),
GrowthModel(
name="Community-Led",
description="Invest in community and content; reduce paid; long-term brand play",
channel_mix={"Organic/SEO": 0.45, "PLG Self-Serve": 0.15, "Outbound SDR": 0.15,
"Paid Search": 0.05, "Events/Field": 0.10, "Partner/Channel": 0.10},
new_mrr_monthly_base=7_000, # Slowest start
monthly_acceleration=0.038,
avg_ltv_cac=4.5,
months_to_steady_state=9, # Community takes longest to activate
notes=[
"Lowest new MRR in months 1-9.",
"Community trust drives lower CAC and higher retention at scale.",
"Best for categories where buyers seek peer validation.",
"Requires dedicated community manager from day one.",
],
),
GrowthModel(
name="Hybrid PLS",
description="PLG self-serve for SMB + sales-assisted for enterprise (Product-Led Sales)",
channel_mix={"Organic/SEO": 0.30, "PLG Self-Serve": 0.28, "Outbound SDR": 0.22,
"Paid Search": 0.08, "Events/Field": 0.06, "Partner/Channel": 0.06},
new_mrr_monthly_base=11_000,
monthly_acceleration=0.035,
avg_ltv_cac=4.1,
months_to_steady_state=4,
notes=[
"PLG handles SMB; sales closes enterprise with PQL signals.",
"Requires clear PQL definition and SDR/PLG handoff process.",
"Best if you have a product with both bottom-up and top-down adoption.",
],
),
]
# ---------------------------------------------------------------------------
# Simulation engine
# ---------------------------------------------------------------------------
def simulate_model(model: GrowthModel, months: int) -> ModelProjection:
snapshots: List[MonthSnapshot] = []
mrr = STARTING_MRR
cumulative_cac = 0.0
cumulative_revenue = 0.0
break_even_month = None
for m in range(1, months + 1):
# Ramp up — new_mrr accelerates each month
if m <= model.months_to_steady_state:
# Ramp phase: linear ramp from 60% to 100% of base
ramp_factor = 0.6 + 0.4 * (m / model.months_to_steady_state)
else:
# Steady state: compound acceleration
months_past_ramp = m - model.months_to_steady_state
ramp_factor = 1.0 + model.monthly_acceleration * months_past_ramp
new_mrr = model.new_mrr_monthly_base * ramp_factor
churned_mrr = mrr * MONTHLY_CHURN_RATE
expansion_mrr = mrr * EXPANSION_RATE
net_new_mrr = new_mrr - churned_mrr + expansion_mrr
mrr = mrr + net_new_mrr
# CAC spend approximation: new_mrr / (avg_deal_mrr) * blended_cac
# Use weighted CAC from channel mix
weighted_cac = _weighted_cac(model.channel_mix)
avg_deal_mrr = 1_500 # Assumption: $1,500 average deal MRR
deals_this_month = new_mrr / avg_deal_mrr
cac_spend = deals_this_month * weighted_cac
cumulative_cac += cac_spend
cumulative_revenue += mrr * GROSS_MARGIN
if break_even_month is None and cumulative_revenue >= cumulative_cac:
break_even_month = m
snapshots.append(MonthSnapshot(
month=m,
mrr=mrr,
new_mrr=new_mrr,
churned_mrr=churned_mrr,
expansion_mrr=expansion_mrr,
net_new_mrr=net_new_mrr,
cumulative_cac_spend=cumulative_cac,
))
return ModelProjection(
model=model,
snapshots=snapshots,
break_even_month=break_even_month,
)
def _weighted_cac(channel_mix: Dict[str, float]) -> float:
channel_cac = {ch.name: ch.cac for ch in CHANNELS}
total = sum(
channel_mix.get(name, 0) * cac
for name, cac in channel_cac.items()
)
weight_sum = sum(channel_mix.values())
return total / weight_sum if weight_sum > 0 else 5_000
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt_mrr(n: float) -> str:
if n >= 1_000_000:
return f".3fM"
return f".1fK"
def fmt_currency(n: float) -> str:
if n >= 1_000_000:
return f".2fM"
if n >= 1_000:
return f".1fK"
return f".0f"
def print_header(title: str) -> None:
width = 78
print("\n" + "=" * width)
print(f" {title}")
print("=" * width)
def print_channel_overview() -> None:
print_header("Current Channel Mix")
print(f" Starting MRR: {fmt_mrr(STARTING_MRR)} | Monthly churn: {MONTHLY_CHURN_RATE:.1%} | Expansion: {EXPANSION_RATE:.1%}/mo")
print()
print(f" {'Channel':<22} {'% MRR':>7} {'CAC':>8} {'Payback':>9} {'Growth/mo':>10}")
print(" " + "-" * 60)
for ch in sorted(CHANNELS, key=lambda c: c.pct_of_new_mrr, reverse=True):
print(
f" {ch.name:<22} {ch.pct_of_new_mrr:>6.0%} "
f"{fmt_currency(ch.cac):>8} {ch.payback_months:>7.0f}mo "
f"{ch.monthly_growth_rate:>9.1%}"
)
def print_model_detail(proj: ModelProjection) -> None:
model = proj.model
print_header(f"Model: {model.name}")
print(f" {model.description}")
if model.notes:
print()
for note in model.notes:
print(f" • {note}")
print()
# Print monthly snapshot (every 3 months + final)
milestones = set(range(3, SIMULATION_MONTHS + 1, 3)) | {SIMULATION_MONTHS}
print(f" {'Month':<7} {'MRR':>10} {'New MRR':>9} {'Churned':>9} {'Expand':>8} {'Net New':>9}")
print(" " + "-" * 56)
for snap in proj.snapshots:
if snap.month in milestones:
print(
f" {snap.month:<7} {fmt_mrr(snap.mrr):>10} "
f"{fmt_mrr(snap.new_mrr):>9} {fmt_mrr(snap.churned_mrr):>9} "
f"{fmt_mrr(snap.expansion_mrr):>8} {fmt_mrr(snap.net_new_mrr):>9}"
)
final = proj.snapshots[-1]
growth_x = final.mrr / STARTING_MRR
arr_final = final.mrr * 12
weighted_cac = _weighted_cac(model.channel_mix)
be = f"Month {proj.break_even_month}" if proj.break_even_month else f"> {SIMULATION_MONTHS}mo"
print()
print(f" Final MRR ({SIMULATION_MONTHS}mo): {fmt_mrr(final.mrr)}")
print(f" Final ARR: {fmt_currency(arr_final)}")
print(f" Growth multiple: {growth_x:.1f}x from starting MRR")
print(f" Weighted blended CAC: {fmt_currency(weighted_cac)}")
print(f" Expected LTV:CAC: {model.avg_ltv_cac:.1f}x")
print(f" Months to steady state:{model.months_to_steady_state}")
print(f" CAC break-even: {be}")
def print_comparison_table(projections: List[ModelProjection]) -> None:
print_header(f"Growth Model Comparison — Month {SIMULATION_MONTHS} Outcomes")
header = (
f" {'Model':<20} {'MRR (final)':>12} {'ARR (final)':>12} "
f"{'Growth':>7} {'LTV:CAC':>8} {'Break-even':>11}"
)
print(header)
print(" " + "-" * 74)
for proj in sorted(projections, key=lambda p: p.snapshots[-1].mrr, reverse=True):
final = proj.snapshots[-1]
growth_x = final.mrr / STARTING_MRR
arr_final = final.mrr * 12
be = f"Mo {proj.break_even_month}" if proj.break_even_month else f">{SIMULATION_MONTHS}mo"
print(
f" {proj.model.name:<20} {fmt_mrr(final.mrr):>12} "
f"{fmt_currency(arr_final):>12} {growth_x:>6.1f}x "
f"{proj.model.avg_ltv_cac:>7.1f}x {be:>11}"
)
def print_channel_mix_impact(projections: List[ModelProjection]) -> None:
print_header("Channel Mix Impact Analysis")
print(" How shifting channel mix changes growth trajectory:\n")
baseline = next((p for p in projections if p.model.name == "Current Mix"), None)
if not baseline:
return
baseline_final_mrr = baseline.snapshots[-1].mrr
for proj in projections:
if proj.model.name == "Current Mix":
continue
final_mrr = proj.snapshots[-1].mrr
delta = final_mrr - baseline_final_mrr
delta_pct = (delta / baseline_final_mrr) * 100
arrow = "↑" if delta > 0 else "↓"
m6_mrr = proj.snapshots[5].mrr if len(proj.snapshots) >= 6 else 0
m6_baseline = baseline.snapshots[5].mrr if len(baseline.snapshots) >= 6 else 0
m6_delta = m6_mrr - m6_baseline
m6_pct = (m6_delta / m6_baseline) * 100 if m6_baseline else 0
m6_arrow = "↑" if m6_delta > 0 else "↓"
print(f" {proj.model.name}:")
print(f" Month 6: {m6_arrow} {abs(m6_pct):.1f}% vs. current ({fmt_mrr(m6_delta)} {'more' if m6_delta > 0 else 'less'} MRR)")
print(f" Month {SIMULATION_MONTHS}: {arrow} {abs(delta_pct):.1f}% vs. current ({fmt_mrr(delta)} {'more' if delta > 0 else 'less'} MRR)")
if proj.model.months_to_steady_state > 4:
print(f" ⚠ Model takes {proj.model.months_to_steady_state} months to reach steady state — short-term dip expected.")
print()
def print_decision_guide(projections: List[ModelProjection]) -> None:
print_header("Decision Guide")
print(" Choose your growth model based on your constraints:\n")
guides = [
("ACV < $5K and fast time-to-value", "PLG-First"),
("ACV > $25K and complex buying process", "Sales-Led Scale"),
("Strong practitioner community exists", "Community-Led"),
("Both SMB self-serve and enterprise buyers", "Hybrid PLS"),
("Uncertain — keep optionality", "Current Mix"),
]
for condition, model_name in guides:
proj = next((p for p in projections if p.model.name == model_name), None)
if proj:
final_mrr = proj.snapshots[-1].mrr
print(f" If: {condition}")
print(f" → Use {model_name} → {fmt_mrr(final_mrr)} MRR at month {SIMULATION_MONTHS}")
print()
print(" Key question before switching models:")
print(" 'Do we have 12-18 months of runway to prove the new model")
print(" while the current model continues in parallel?'")
print(" If no → optimize current model. Don't switch.")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
print_channel_overview()
projections = [simulate_model(model, SIMULATION_MONTHS) for model in GROWTH_MODELS]
for proj in projections:
print_model_detail(proj)
print_comparison_table(projections)
print_channel_mix_impact(projections)
print_decision_guide(projections)
print("\n" + "=" * 78)
print(" Notes:")
print(f" Starting MRR: {fmt_mrr(STARTING_MRR)}")
print(f" Simulation: {SIMULATION_MONTHS} months")
print(f" Churn: {MONTHLY_CHURN_RATE:.1%}/mo ({MONTHLY_CHURN_RATE*12:.0%} annualized)")
print(f" Expansion: {EXPANSION_RATE:.1%}/mo of existing MRR")
print(f" Gross margin: {GROSS_MARGIN:.0%}")
print(" Acceleration rates are estimates — validate against your actuals.")
print("=" * 78 + "\n")
if __name__ == "__main__":
main()
FILE:cmo-advisor/scripts/marketing_budget_modeler.py
#!/usr/bin/env python3
"""
Marketing Budget Modeler
------------------------
Allocates marketing budget across channels based on CAC efficiency and
target MQL volume. Models conservative / moderate / aggressive scenarios.
Usage:
python marketing_budget_modeler.py
Inputs (edit INPUTS section below or extend with argparse):
- Annual revenue target (new ARR)
- Average selling price (ASP)
- Conversion rates by funnel stage
- Historical CAC per channel
- Channel capacity constraints (max MQLs the channel can realistically produce)
Outputs:
- Required MQL volume by channel
- Budget allocation per channel per scenario
- LTV:CAC and payback period per channel
- Summary table across scenarios
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import Dict, List, Tuple
# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------
@dataclass
class Channel:
name: str
cac: float # Customer acquisition cost ($)
max_mqls_per_month: int # Realistic capacity ceiling (MQLs/month)
mql_to_close_rate: float # Combined MQL → closed-won rate (0.0–1.0)
payback_months: float # Based on ARPU × gross margin
ltv: float # Lifetime value ($)
trend: str = "stable" # "improving" | "stable" | "declining"
@dataclass
class FunnelRates:
mql_to_sal: float # MQL → Sales Accepted Lead
sal_to_sql: float # SAL → Sales Qualified Lead
sql_to_opp: float # SQL → Opportunity
opp_to_close: float # Opportunity → Closed-Won
@property
def mql_to_close(self) -> float:
return self.mql_to_sal * self.sal_to_sql * self.sql_to_opp * self.opp_to_close
@dataclass
class ScenarioResult:
name: str
total_budget: float
channel_budgets: Dict[str, float]
channel_mqls: Dict[str, int]
projected_customers: int
projected_arr: float
blended_cac: float
notes: List[str] = field(default_factory=list)
# ---------------------------------------------------------------------------
# INPUTS — edit these
# ---------------------------------------------------------------------------
TARGET_NEW_ARR = 3_000_000 # New ARR to generate this year ($)
ASP_ANNUAL = 18_000 # Average annual contract value ($)
GROSS_MARGIN = 0.75 # Product gross margin (%)
ARPU_MONTHLY = ASP_ANNUAL / 12 # Monthly revenue per account
FUNNEL = FunnelRates(
mql_to_sal=0.65,
sal_to_sql=0.45,
sql_to_opp=0.75,
opp_to_close=0.27,
)
# LTV = ARPU_monthly × gross_margin / monthly_churn_rate
MONTHLY_CHURN = 0.012 # ~14% annual churn
LTV = (ARPU_MONTHLY * GROSS_MARGIN) / MONTHLY_CHURN
CHANNELS: List[Channel] = [
Channel(
name="Organic SEO",
cac=1_800,
max_mqls_per_month=80,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(1_800 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="improving",
),
Channel(
name="Paid Search",
cac=6_200,
max_mqls_per_month=60,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(6_200 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="stable",
),
Channel(
name="Paid Social (LinkedIn)",
cac=8_500,
max_mqls_per_month=35,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(8_500 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="declining",
),
Channel(
name="Outbound SDR",
cac=5_100,
max_mqls_per_month=50,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(5_100 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="stable",
),
Channel(
name="Events / Field",
cac=9_800,
max_mqls_per_month=25,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(9_800 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="stable",
),
Channel(
name="Partner / Channel",
cac=3_400,
max_mqls_per_month=30,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(3_400 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="improving",
),
Channel(
name="Content / Inbound",
cac=2_600,
max_mqls_per_month=45,
mql_to_close_rate=FUNNEL.mql_to_close,
payback_months=(2_600 / (ARPU_MONTHLY * GROSS_MARGIN)),
ltv=LTV,
trend="improving",
),
]
# ---------------------------------------------------------------------------
# Core calculations
# ---------------------------------------------------------------------------
def customers_needed(target_arr: float, asp: float) -> int:
return math.ceil(target_arr / asp)
def mqls_needed_total(customers: int, mql_to_close: float) -> int:
return math.ceil(customers / mql_to_close)
def ltv_to_cac(ltv: float, cac: float) -> float:
return ltv / cac if cac > 0 else 0.0
def score_channel(ch: Channel) -> float:
"""
Score a channel for budget priority.
Higher = more efficient. Used to rank allocation order.
Factors: LTV:CAC ratio, trend multiplier, capacity.
"""
ratio = ltv_to_cac(ch.ltv, ch.cac)
trend_mult = {"improving": 1.2, "stable": 1.0, "declining": 0.7}.get(ch.trend, 1.0)
return ratio * trend_mult
def allocate_mqls(
channels: List[Channel],
total_mqls_needed: int,
budget_multiplier: float = 1.0,
) -> Tuple[Dict[str, int], Dict[str, float]]:
"""
Allocate MQL targets across channels in priority order (best LTV:CAC first).
budget_multiplier: 0.7 = conservative, 1.0 = moderate, 1.3 = aggressive.
Returns (channel → MQLs, channel → budget).
"""
ranked = sorted(channels, key=score_channel, reverse=True)
remaining = total_mqls_needed
channel_mqls: Dict[str, int] = {}
channel_budget: Dict[str, float] = {}
for ch in ranked:
if remaining <= 0:
channel_mqls[ch.name] = 0
channel_budget[ch.name] = 0.0
continue
# Apply capacity ceiling scaled by multiplier (aggressive = push capacity)
capacity = int(ch.max_mqls_per_month * 12 * budget_multiplier)
allocated = min(remaining, capacity)
channel_mqls[ch.name] = allocated
channel_budget[ch.name] = allocated * ch.cac
remaining -= allocated
return channel_mqls, channel_budget
def build_scenario(
name: str,
channels: List[Channel],
total_mqls: int,
multiplier: float,
notes: List[str],
) -> ScenarioResult:
channel_mqls, channel_budget = allocate_mqls(channels, total_mqls, multiplier)
total_budget = sum(channel_budget.values())
total_mqls_allocated = sum(channel_mqls.values())
projected_customers = math.floor(total_mqls_allocated * FUNNEL.mql_to_close)
projected_arr = projected_customers * ASP_ANNUAL
# Blended CAC = total budget / customers acquired
blended_cac = total_budget / projected_customers if projected_customers > 0 else 0.0
return ScenarioResult(
name=name,
total_budget=total_budget,
channel_budgets=channel_budget,
channel_mqls=channel_mqls,
projected_customers=projected_customers,
projected_arr=projected_arr,
blended_cac=blended_cac,
notes=notes,
)
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt_currency(n: float) -> str:
if n >= 1_000_000:
return f".2fM"
if n >= 1_000:
return f".1fK"
return f".0f"
def fmt_ratio(n: float) -> str:
return f"{n:.1f}x"
def print_header(title: str) -> None:
width = 72
print("\n" + "=" * width)
print(f" {title}")
print("=" * width)
def print_channel_table(channels: List[Channel]) -> None:
print_header("Channel Analysis — Current State")
header = f"{'Channel':<25} {'CAC':>8} {'Payback':>9} {'LTV:CAC':>8} {'Cap/mo':>7} {'Trend':>10}"
print(header)
print("-" * 72)
for ch in sorted(channels, key=score_channel, reverse=True):
ratio = ltv_to_cac(ch.ltv, ch.cac)
flag = ""
if ratio < 1:
flag = " ⚠ LOSS"
elif ratio >= 6:
flag = " ★ STRONG"
elif ratio >= 3:
flag = " ✓"
print(
f"{ch.name:<25} {fmt_currency(ch.cac):>8} "
f"{ch.payback_months:>7.1f}mo {fmt_ratio(ratio):>8} "
f"{ch.max_mqls_per_month:>7} {ch.trend:>10}{flag}"
)
def print_funnel_summary(customers: int, mqls: int) -> None:
print_header("Funnel Requirements")
print(f" Target new ARR: {fmt_currency(TARGET_NEW_ARR)}")
print(f" Average selling price: {fmt_currency(ASP_ANNUAL)}")
print(f" New customers needed: {customers}")
print(f" Funnel MQL→Close rate: {FUNNEL.mql_to_close:.1%}")
print(f" Total MQLs needed: {mqls}")
print(f"\n Funnel stage rates:")
print(f" MQL → SAL: {FUNNEL.mql_to_sal:.0%}")
print(f" SAL → SQL: {FUNNEL.mql_to_sal * FUNNEL.sal_to_sql:.0%}")
print(f" SQL → Opportunity: {FUNNEL.mql_to_sal * FUNNEL.sal_to_sql * FUNNEL.sql_to_opp:.0%}")
print(f" Opportunity → Close: {FUNNEL.mql_to_close:.0%}")
print(f"\n LTV (estimated): {fmt_currency(LTV)}")
print(f" Monthly churn: {MONTHLY_CHURN:.1%} ({MONTHLY_CHURN*12:.0%} annualized)")
def print_scenario(result: ScenarioResult, channels: List[Channel]) -> None:
print_header(f"Scenario: {result.name}")
print(f" Total marketing budget: {fmt_currency(result.total_budget)}")
print(f" Projected customers: {result.projected_customers}")
print(f" Projected new ARR: {fmt_currency(result.projected_arr)}")
print(f" Blended CAC: {fmt_currency(result.blended_cac)}")
blended_ltv_cac = LTV / result.blended_cac if result.blended_cac > 0 else 0
blended_payback = result.blended_cac / (ARPU_MONTHLY * GROSS_MARGIN)
print(f" Blended LTV:CAC: {fmt_ratio(blended_ltv_cac)}", end="")
if blended_ltv_cac < 1:
print(" ⚠ BELOW BREAK-EVEN")
elif blended_ltv_cac < 3:
print(" △ MARGINAL")
elif blended_ltv_cac >= 3:
print(" ✓ HEALTHY")
else:
print()
print(f" Blended payback: {blended_payback:.1f} months")
if result.notes:
print(f"\n Notes:")
for note in result.notes:
print(f" • {note}")
print(f"\n {'Channel':<25} {'MQLs':>6} {'Budget':>10} {'% of Budget':>12} {'LTV:CAC':>8}")
print(" " + "-" * 65)
for ch in sorted(channels, key=score_channel, reverse=True):
mqls = result.channel_mqls.get(ch.name, 0)
budget = result.channel_budgets.get(ch.name, 0.0)
pct = (budget / result.total_budget * 100) if result.total_budget > 0 else 0
ratio = ltv_to_cac(ch.ltv, ch.cac)
print(
f" {ch.name:<25} {mqls:>6} {fmt_currency(budget):>10} "
f"{pct:>11.1f}% {fmt_ratio(ratio):>8}"
)
def print_scenario_comparison(scenarios: List[ScenarioResult]) -> None:
print_header("Scenario Comparison")
header = f"{'Scenario':<18} {'Budget':>10} {'Customers':>10} {'ARR':>10} {'Blended CAC':>12} {'LTV:CAC':>8} {'Payback':>9}"
print(header)
print("-" * 82)
for s in scenarios:
blended_ltv_cac = LTV / s.blended_cac if s.blended_cac > 0 else 0
blended_payback = s.blended_cac / (ARPU_MONTHLY * GROSS_MARGIN)
print(
f"{s.name:<18} {fmt_currency(s.total_budget):>10} "
f"{s.projected_customers:>10} {fmt_currency(s.projected_arr):>10} "
f"{fmt_currency(s.blended_cac):>12} {fmt_ratio(blended_ltv_cac):>8} "
f"{blended_payback:>7.1f}mo"
)
def print_recommendations(channels: List[Channel]) -> None:
print_header("Channel Recommendations")
scale = [ch for ch in channels if score_channel(ch) >= 1.5 and ch.trend in ("improving", "stable")]
hold = [ch for ch in channels if 0.8 <= score_channel(ch) < 1.5 or (ch.trend == "stable" and ltv_to_cac(ch.ltv, ch.cac) >= 3)]
cut = [ch for ch in channels if ltv_to_cac(ch.ltv, ch.cac) < 2 or ch.trend == "declining"]
# Deduplicate
hold = [ch for ch in hold if ch not in scale]
cut = [ch for ch in cut if ch not in scale and ch not in hold]
if scale:
print(" SCALE (strong LTV:CAC, improving or stable trend):")
for ch in scale:
print(f" + {ch.name} [LTV:CAC {fmt_ratio(ltv_to_cac(ch.ltv, ch.cac))}, payback {ch.payback_months:.0f}mo]")
if hold:
print(" HOLD (monitor — adequate but not outstanding):")
for ch in hold:
print(f" = {ch.name} [LTV:CAC {fmt_ratio(ltv_to_cac(ch.ltv, ch.cac))}, trend: {ch.trend}]")
if cut:
print(" CUT or REDUCE (poor LTV:CAC or declining):")
for ch in cut:
print(f" - {ch.name} [LTV:CAC {fmt_ratio(ltv_to_cac(ch.ltv, ch.cac))}, trend: {ch.trend}]")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
customers = customers_needed(TARGET_NEW_ARR, ASP_ANNUAL)
total_mqls = mqls_needed_total(customers, FUNNEL.mql_to_close)
print_channel_table(CHANNELS)
print_funnel_summary(customers, total_mqls)
scenarios = [
build_scenario(
name="Conservative",
channels=CHANNELS,
total_mqls=total_mqls,
multiplier=0.7,
notes=[
"Prioritizes lowest CAC channels only.",
"May not reach MQL target — expect ~70% of goal.",
"Best for capital-constrained orgs or short runway.",
],
),
build_scenario(
name="Moderate",
channels=CHANNELS,
total_mqls=total_mqls,
multiplier=1.0,
notes=[
"Balanced allocation — efficiency-first but full MQL target.",
"Recommended baseline. Revisit Q2 based on actuals.",
],
),
build_scenario(
name="Aggressive",
channels=CHANNELS,
total_mqls=total_mqls,
multiplier=1.4,
notes=[
"Pushes all channels toward capacity ceiling.",
"Higher spend on lower-efficiency channels to hit volume.",
"Requires > 18-month runway to justify payback period.",
],
),
]
for scenario in scenarios:
print_scenario(scenario, CHANNELS)
print_scenario_comparison(scenarios)
print_recommendations(CHANNELS)
print("\n" + "=" * 72)
print(" Key questions before finalizing budget:")
print(" 1. What is the payback period the CFO/board will accept?")
print(" 2. Is CAC for declining-trend channels actually recoverable?")
print(" 3. Does the moderate scenario require sales headcount increase?")
print(" 4. Which channels have capacity to absorb 20% more spend?")
print("=" * 72 + "\n")
if __name__ == "__main__":
main()
FILE:company-os/SKILL.md
---
name: "company-os"
description: "The meta-framework for how a company runs — the connective tissue between all C-suite roles. Covers operating system selection (EOS, Scaling Up, OKR-native, hybrid), accountability charts, scorecards, meeting pulse, issue resolution, and 90-day rocks. Use when setting up company operations, selecting a management framework, designing meeting rhythms, building accountability systems, implementing OKRs, or when user mentions EOS, Scaling Up, operating system, L10 meetings, rocks, scorecard, accountability chart, or quarterly planning."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: company-operations
updated: 2026-03-05
frameworks: os-comparison, implementation-guide
---
# Company Operating System
The operating system is the collection of tools, rhythms, and agreements that determine how the company functions. Every company has one — most just don't know what it is. Making it explicit makes it improvable.
## Keywords
operating system, EOS, Entrepreneurial Operating System, Scaling Up, Rockefeller Habits, OKR, Holacracy, L10 meeting, rocks, scorecard, accountability chart, issues list, IDS, meeting pulse, quarterly planning, weekly scorecard, management framework, company rhythm, traction, Gino Wickman, Verne Harnish
## Why This Matters
Most operational dysfunction isn't a people problem — it's a system problem. When:
- The same issues recur every week: no issue resolution system
- Meetings feel pointless: no structured meeting pulse
- Nobody knows who owns what: no accountability chart
- Quarterly goals slip: rocks aren't real commitments
Fix the system. The people will operate better inside it.
## The Six Core Components
Every effective operating system has these six, regardless of which framework you choose:
### 1. Accountability Chart
Not an org chart. An accountability chart answers: "Who owns this outcome?"
**Key distinction:** One person owns each function. Multiple people may work in it. Ownership means the buck stops with one person.
**Structure:**
```
CEO
├── Sales (CRO/VP Sales)
│ ├── Inbound pipeline
│ └── Outbound pipeline
├── Product & Engineering (CTO/CPO)
│ ├── Product roadmap
│ └── Engineering delivery
├── Operations (COO)
│ ├── Customer success
│ └── Finance & Legal
└── People (CHRO/VP People)
├── Recruiting
└── People operations
```
**Rules:**
- No shared ownership. "Alice and Bob both own it" means nobody owns it.
- One person can own multiple seats at early stages. That's fine. Just be explicit.
- Revisit quarterly as you scale. Ownership shifts as the company grows.
**Build it in a workshop:**
1. List all functions the company performs
2. Assign one owner per function — no exceptions
3. Identify gaps (functions nobody owns) and overlaps (functions two people think they own)
4. Publish it. Update it when something changes.
### 2. Scorecard
Weekly metrics that tell you if the company is on track. Not monthly. Not quarterly. Weekly.
**Rules:**
- 5–15 metrics maximum. More than 15 and nothing gets attention.
- Each metric has an owner and a weekly target (not a range — a number).
- Red/yellow/green status. Not paragraphs.
- The scorecard is discussed at the leadership team weekly meeting. Only red metrics get discussion time.
**Example scorecard structure:**
| Metric | Owner | Target | This Week | Status |
|--------|-------|--------|-----------|--------|
| New MRR | CRO | €50K | €43K | 🔴 |
| Churn | CS Lead | < 1% | 0.8% | 🟢 |
| Active users | CPO | 2,000 | 2,150 | 🟢 |
| Deployments | CTO | 3/week | 3 | 🟢 |
| Open critical bugs | CTO | 0 | 2 | 🔴 |
| Runway | CFO | > 18mo | 16mo | 🟡 |
**Anti-pattern:** Measuring everything. If you track 40 KPIs, you're watching, not managing.
### 3. Meeting Pulse
The meeting rhythm that drives the company. Not optional — the pulse is what keeps the company alive.
**The full rhythm:**
| Meeting | Frequency | Duration | Who | Purpose |
|---------|-----------|----------|-----|---------|
| Daily standup | Daily | 15 min | Each team | Blockers only |
| L10 / Leadership sync | Weekly | 90 min | Leadership team | Scorecard + issues |
| Department review | Monthly | 60 min | Dept + leadership | OKR progress |
| Quarterly planning | Quarterly | 1–2 days | Leadership | Set rocks, review strategy |
| Annual planning | Annual | 2–3 days | Leadership | 1-year + 3-year vision |
**The L10 meeting (Weekly Leadership Sync):**
Named for the goal of each meeting being a 10/10. Fixed agenda:
1. Good news (5 min) — personal + business
2. Scorecard review (5 min) — flag red items only
3. Rock review (5 min) — on/off track for each rock
4. Customer/employee headlines (5 min)
5. Issues list (60 min) — IDS (see below)
6. To-dos review (5 min) — last week's commitments
7. Conclude (5 min) — rate the meeting 1–10, what would make it a 10 next time
### 4. Issue Resolution (IDS)
The core problem-solving loop. Maximum 15 minutes per issue.
**IDS: Identify, Discuss, Solve**
- **Identify:** What is the actual issue? (Not the symptom — the root cause) State it in one sentence.
- **Discuss:** Relevant facts + perspectives. Time-boxed. When discussion starts repeating, stop.
- **Solve:** One owner. One action. One due date. Written on the to-do list.
**Anti-patterns:**
- "Let's take this offline" — most things taken offline never get resolved
- Discussing without deciding — a great discussion with no action item is wasted time
- Revisiting decided issues — once solved, it leaves the list. Reopen only with new information.
**The Issues List:** A running, prioritized list of all unresolved issues. Owned by the leadership team. Reviewed and pruned weekly. If an issue has been on the list for 3+ meetings and hasn't been discussed, it's either not a real issue or it's too scary to address — both deserve attention.
### 5. Rocks (90-Day Priorities)
Rocks are the 3–7 most important things each person must accomplish in the next 90 days. They're not the job description — they're the things that move the company forward.
**Why 90 days?** Long enough for meaningful progress. Short enough to stay real.
**Rock rules:**
- Each person: 3–7 rocks maximum. More than 7 and none get done.
- Company-level rocks (shared priorities): 3–7 for the leadership team
- Each rock is binary: done or not done. No "60% complete."
- Set at the quarterly planning session. Reviewed weekly (on/off track).
**Bad rock:** "Improve our sales process"
**Good rock:** "Implement Salesforce CRM with full pipeline stages and weekly reporting by March 31"
**Rock vs. to-do:** A to-do takes one action. A rock takes 90 days of consistent work.
### 6. Communication Cadence
Who gets what information, when, and how.
| Audience | What | When | Format |
|----------|------|------|--------|
| All employees | Company update | Monthly | Written + Q&A |
| All employees | Quarterly results + next priorities | Quarterly | All-hands |
| Leadership team | Scorecard | Weekly | Dashboard |
| Board | Company performance | Monthly | Board memo |
| Investors | Key metrics + narrative | Monthly or quarterly | Investor update |
| Customers | Product updates | Per release | Release notes |
**Default rule:** If you're deciding whether to share something internally, share it. The cost of under-communication always exceeds the cost of over-communication inside a company.
---
## Operating System Selection
See `references/os-comparison.md` for full comparison. Quick guide:
| If you are... | Consider... |
|---------------|-------------|
| 10–250 person company, founder-led, operational chaos | EOS / Traction |
| Ambitious growth company, need rigorous strategy cascade | Scaling Up |
| Tech company, engineering culture, hypothesis-driven | OKR-native |
| Decentralized, flat, high autonomy | Holacracy (only if you're patient) |
| None of the above quite fit | Custom hybrid |
---
## Implementation Roadmap
Don't implement everything at once. See `references/implementation-guide.md` for the full 90-day plan.
**Quick start (first 30 days):**
1. Build the accountability chart (1 workshop, 2 hours)
2. Define 5–10 weekly scorecard metrics (leadership team alignment, 1 hour)
3. Start the weekly L10 meeting (no prep — just start)
These three alone will improve coordination more than most companies achieve in a year.
---
## Common Failure Modes
**Partial implementation:** "We do OKRs but skip the weekly check-in." Half an operating system is worse than none — it creates theater without accountability.
**Meeting fatigue:** Adding the full rhythm on top of existing meetings. Start by replacing meetings, not adding them.
**Metric overload:** Starting with 30 KPIs because "they all matter." Start with 5. Add when the cadence is established.
**Rock inflation:** Setting 12 rocks per person because "everything is a priority." When everything is a priority, nothing is. Hard limit: 7.
**Leader non-compliance:** Leadership team skips the L10 or doesn't follow IDS. The operating system mirrors the respect leadership gives it. If leaders don't take it seriously, nobody will.
**Annual planning without quarterly review:** Setting annual goals and checking in at year-end. Quarterly is the minimum review cycle for any meaningful goal.
---
## Integration with C-Suite
The company OS is the connective tissue. Every other role depends on it:
| C-Suite Role | OS Dependency |
|-------------|---------------|
| CEO | Sets vision that feeds into 1-year plan and rocks |
| COO | Owns the meeting pulse and issue resolution cadence |
| CFO | Owns the financial metrics in the scorecard |
| CTO | Owns engineering rocks and tech scorecard metrics |
| CHRO | Owns people metrics (attrition, hiring velocity) in scorecard |
| Culture Architect | Culture rituals plug into the meeting pulse |
| Strategic Alignment Engine | Validates that team rocks cascade from company rocks |
---
## Key Questions for the Operating System
- "If I asked five different team leads what the company's top 3 priorities are this quarter, would they give the same answers?"
- "What was the most important issue raised in last week's leadership meeting? Was it resolved or is it still open?"
- "Name a metric that would tell us by Friday whether this week was a good week. Do we track it?"
- "Who owns customer churn? Can you name that person without hesitation?"
- "When was the last time we updated the accountability chart?"
## Detailed References
- `references/os-comparison.md` — EOS vs Scaling Up vs OKRs vs Holacracy vs hybrid
- `references/implementation-guide.md` — 90-day implementation plan
FILE:company-os/references/implementation-guide.md
# Company Operating System — 90-Day Implementation Guide
Don't implement everything at once. The fastest path to failure is trying to launch the full operating system in week one. Build incrementally. Let the team experience wins before adding complexity.
---
## Before You Start
### Prerequisites
**Leadership alignment (non-negotiable):**
Every member of the leadership team must understand why you're doing this and commit to running the system. One holdout destroys the whole model. If the CFO skips the L10 meetings, the system won't work.
**Current state audit:**
- What meetings currently exist? Which can be replaced?
- Who owns which functions today? (Even informally)
- What metrics are being tracked? (Even inconsistently)
**Assign an OS owner:**
One person is responsible for the implementation and ongoing maintenance of the operating system. Usually the COO or CEO (at smaller companies). This is not a committee job.
---
## Week 1–2: Accountability Chart + Scorecard
### Accountability Chart Workshop (Week 1)
**Duration:** 2–3 hours, full leadership team
**Step 1 — List all functions (30 min)**
On a whiteboard, list every function the company performs:
- Sales (inbound, outbound, partnerships)
- Marketing (content, paid, brand)
- Product (roadmap, design, research)
- Engineering (frontend, backend, devops)
- Customer success (onboarding, support, retention)
- Finance (accounting, FP&A, legal)
- People (recruiting, HR, culture)
- Operations (processes, tools, facilities)
**Step 2 — Assign owners (45 min)**
For each function: "Who is the one person ultimately accountable?" Write their name.
Rules: One name only. No joint ownership. One person can own multiple functions at small scale.
**Step 3 — Identify gaps and overlaps (30 min)**
- **Gaps:** Functions with no owner → Who should own them? Or do we need a hire?
- **Overlaps:** Two people said they own the same thing → Resolve now, not later.
**Step 4 — Publish and socialize (Week 2)**
Share with the full company. Explain what an accountability chart is and isn't.
"This is about clarity, not hierarchy. It tells everyone who to go to for each function."
**Output:** A documented accountability chart. Use a simple tool (Miro, Google Slides, Ninety.io).
---
### Scorecard Design (Week 2)
**Duration:** 90 minutes, leadership team
**Step 1 — List candidate metrics (30 min)**
Each leader lists 3–5 metrics they already track or wish they tracked. No filtering yet.
**Step 2 — Filter to 5–15 (30 min)**
Criteria: Is it measurable weekly? Does it tell us if the company is healthy? Does one person own it?
Drop: metrics that are monthly only, metrics without a clear owner, metrics that measure activity not outcomes.
**Step 3 — Set weekly targets (20 min)**
For each metric: what's the weekly target? Not a range — a number. Red/yellow/green thresholds.
**Step 4 — Assign owners (10 min)**
Every metric has one owner who is responsible for reporting it weekly.
**Output:** A scorecard document. 5–15 metrics, owner, target, weekly tracking column.
**First scorecard run:** Week 2 or 3. It won't be perfect. That's fine.
---
## Week 3–4: Meeting Pulse (Start With L10)
Don't start all the meetings at once. Start with the weekly L10. Replace existing leadership syncs.
### L10 Meeting Setup
**Schedule:** Same day, same time, every week. Non-negotiable attendance.
**Duration:** 90 minutes. No more, no less.
**Facilitator:** Rotate or assign to COO/CEO. The facilitator keeps time and follows the agenda.
**Fixed agenda:**
1. **Good news** (5 min) — One personal, one business from each person. No skipping.
2. **Scorecard review** (5 min) — Traffic light only. Red items go to the issues list.
3. **Rock review** (5 min) — Each person: "on track" or "off track." No justification needed at this step.
4. **Customer/employee headlines** (5 min) — One sentence each. No reports.
5. **Issues** (60 min) — IDS process. Prioritize the top 3–5 issues. Solve them.
6. **To-do review** (5 min) — Review last week's commitments (done/not done). No excuses, just data.
7. **Conclude** (5 min) — Rate the meeting 1–10. What would make next week better?
**First L10 meeting:**
It will feel awkward. Run through the agenda anyway. The team needs the repetition to internalize it. By week 4, it should feel natural.
### Issues List Setup
Create a shared document (Notion, Google Docs, or dedicated tool):
- Issue title
- Priority (High / Medium / Low)
- Status (Open / In progress / Solved)
- Owner (once assigned)
- Due date
At the first L10, generate the issues list by asking: "What's getting in our way right now?" Expect 10–20 items on the first pass.
---
## Week 5–8: Rocks and Quarterly Planning
### Quarterly Planning Session (end of Week 5 or start of Week 6)
**Duration:** 4–8 hours (or 2 × 4-hour days for larger teams)
**Who:** Full leadership team
**Session structure:**
**Part 1: Review previous quarter (60–90 min)**
- What rocks were completed? What were dropped?
- What did we learn?
- What changed in the market or company?
**Part 2: Confirm or update company direction (60 min)**
- Is the 1-year goal still valid?
- Any major strategy shifts needed?
- Update the V/TO or OPSP if using EOS or Scaling Up.
**Part 3: Set company rocks (90 min)**
- Brainstorm: What are the 3–7 most important things to accomplish this quarter?
- Prioritize. Be ruthless. 3 rocks done > 7 rocks started.
- Each rock: clear owner, clear definition of done, 90-day timeline.
**Part 4: Set individual rocks (60 min)**
- Each leader sets their 3–7 rocks (aligned with company rocks where possible)
- Share with group: dependencies? Conflicts? Overloaded people?
**Part 5: Communicate (Week 6)**
- Share company rocks with the full organization within 1 week
- Each team sets their own rocks, cascaded from company rocks (3–5 per team)
**Rock template:**
```
Rock: [What you'll accomplish]
Owner: [One person]
Due date: [Specific date within the quarter]
Definition of done: [How we'll know it's complete]
Dependencies: [What else needs to happen first]
```
---
## Week 9–12: Issue Resolution Mastery + Communication Cadence
By now the L10 should be running smoothly. Weeks 9–12 focus on deepening IDS skills and establishing the broader communication cadence.
### IDS Practice
The issue resolution process often degrades in weeks 5–8. Common problems:
- Issues discussed but never solved (no clear action item)
- Same issues recurring (root cause not addressed)
- Too many issues, not enough resolution (prioritization failing)
**IDS calibration exercise (Week 9):**
In the next L10, after each issue is "solved," ask:
- "Is this actually solved, or are we postponing it?"
- "What's the specific action? Who owns it? When is it due?"
- "Is this the real issue, or a symptom of something deeper?"
### Communication Cadence Setup
Build out the full communication calendar:
| Communication | Frequency | Owner | Format | Tool |
|---------------|-----------|-------|--------|------|
| Company all-hands | Monthly | CEO | Update + Q&A | Video call |
| Quarterly planning results | Quarterly | CEO/COO | Written + live | Notion + all-hands |
| Board update | Monthly | CEO + CFO | Board memo | Doc |
| Investor update | Monthly | CEO + CFO | Email | Template |
| Department L10s | Weekly | Dept lead | L10 format | In-person / Zoom |
| Daily standups | Daily | Team leads | 15 min | Team call |
**Company all-hands template:**
1. State of the company (financial health, key metrics) — 10 min
2. Quarterly rocks: what we committed to, where we stand — 10 min
3. Wins and recognitions — 5 min
4. What's coming next quarter — 10 min
5. Q&A — 15–25 min
---
## Post-90 Days: Refinement and Optimization
### Month 4 retrospective
After the first full quarter, run a retrospective on the operating system itself:
- What's working? What isn't?
- Which meetings should continue as-is? Which need adjustment?
- Is the scorecard measuring the right things?
- Are rocks the right size and specificity?
- What should we add next?
### Scorecard evolution
By month 4, you'll know which metrics matter most. Add 2–3 that are missing. Remove metrics that nobody uses for decisions.
### L10 health check
Rate your L10 meetings over the first quarter:
- Average rating < 7: The agenda isn't being followed or issues aren't being resolved. Diagnose.
- Average rating 7–8: Normal. Keep building discipline.
- Average rating > 8: The team is engaged. Start extending the system to department level.
### Department L10s (Month 4+)
Once leadership L10 is running well, cascade the meeting structure:
- Each department runs their own weekly L10
- Department rocks cascade from company rocks
- Issues that cross departments are escalated to leadership L10
### Year 1 annual planning
End of year 1: run a full-day annual planning session.
- Review the year: what did we accomplish? What did we miss? What did we learn?
- Update 3-year vision (has it changed?)
- Set next year's annual goals
- Set Q1 rocks
- Celebrate. Seriously — mark the milestone.
---
## Implementation Anti-Patterns
**Skipping the accountability chart:** Without ownership clarity, every other system breaks down. Do this first.
**Building a perfect scorecard before starting:** Start with 5 imperfect metrics. Improve over time.
**Not replacing existing meetings:** Adding L10 on top of 3 existing meetings creates meeting overload. Cancel the redundant ones.
**Leader non-participation:** If one leader consistently skips or is disengaged, the system won't work. Address this directly — it's a culture issue, not a calendar issue.
**Changing the L10 agenda:** The agenda works because of repetition. Resist the urge to customize it for the first 6 months.
**Rocks without accountability:** If nobody checks rocks at the L10 ("on track / off track"), they become wish lists. The weekly review is what makes them real.
FILE:company-os/references/os-comparison.md
# Operating System Comparison
Side-by-side analysis of the major company operating frameworks.
---
## Overview
| Framework | Origin | Best fit | Implementation time | Cost |
|-----------|--------|----------|---------------------|------|
| EOS | Gino Wickman, 2007 | 10–250 employees, founder-led | 2–3 years full adoption | Free (DIY) to $25K+/year (implementer) |
| Scaling Up | Verne Harnish, 2002 | Growth-stage, strategic focus | 1–2 years | Free (DIY) to $15K+/year (coach) |
| OKR-native | Andy Grove / Google | Tech companies, product orgs | 3–6 months | Free |
| Holacracy | Brian Robertson, 2007 | Flat, autonomous organizations | 2–4 years | $5K–$50K+ (certification) |
| Custom hybrid | You | When the above don't fit exactly | Ongoing | Whatever you invest |
---
## 1. EOS — Entrepreneurial Operating System
**Book:** *Traction* by Gino Wickman
### Core principles
EOS is built on Six Components:
1. **Vision** — Where are you going? (V/TO: Vision/Traction Organizer)
2. **People** — Right people, right seats
3. **Data** — Scorecard with weekly metrics
4. **Issues** — Surface and resolve with IDS
5. **Process** — Document core processes
6. **Traction** — Rocks + meeting pulse (L10)
### Signature tools
- **V/TO (Vision/Traction Organizer):** 2-page strategy doc. Core values, core focus, 10-year target, 3-year picture, 1-year plan, quarterly rocks, issues.
- **Accountability Chart:** Who owns what function (not org chart)
- **L10 meeting:** Weekly 90-minute leadership sync (Level 10 = aim for 10/10)
- **Rocks:** 90-day priority commitments (3–7 per person)
- **IDS:** Identify, Discuss, Solve (issue resolution, max 15 min per issue)
### Strengths
- **Operationally focused.** If your problem is execution chaos, EOS addresses it directly.
- **Accessible.** The book is practical. You can DIY it without a coach.
- **Community.** Large network of implementers, tools (Ninety.io, EOS Worldwide), and practitioners.
- **Simple enough to actually use.** No complex methodology. Most teams are functional within 6 months.
### Limitations
- **Strategic depth is shallow.** The V/TO is good for direction but doesn't replace real strategy work.
- **Doesn't scale beyond ~250.** Designed for entrepreneurial companies. Gets cumbersome at enterprise scale.
- **Assumes a cohesive leadership team.** If trust is broken at the top, EOS won't fix it.
- **Facilitator dependency.** Many companies benefit from an EOS Implementer (external coach), which adds cost.
### Best fit
- 10–150 person companies
- Founder-led, operational dysfunction
- Teams that can't stay on the same page
- Companies with recurring issues that never get resolved
- First real "operating system" for a company that's been running on vibes
### Not ideal if
- You need sophisticated strategic planning
- You're > 250 people and already have ops infrastructure
- Your team resists structured methodology
---
## 2. Scaling Up (Rockefeller Habits 2.0)
**Book:** *Scaling Up* by Verne Harnish
### Core principles
Built on four Decisions:
1. **People** — Core values, talent management, Topgrading
2. **Strategy** — One-Page Strategic Plan (OPSP), 7 Strata of Strategy
3. **Execution** — Priorities (rocks), meeting rhythm, critical numbers
4. **Cash** — Power of One, Cash Acceleration Strategies (CAS)
### Signature tools
- **One-Page Strategic Plan (OPSP):** Annual and quarterly goals on one page. More strategic than EOS's V/TO.
- **7 Strata of Strategy:** Competitive positioning, core customer, brand promise, X-factor (10x advantage), profit per X, BHAG, critical numbers.
- **Meeting rhythm:** Daily (5–15 min), weekly, monthly, quarterly, annual — with specific templates.
- **Critical number:** One metric that, if improved, fixes everything else.
- **Cash acceleration:** CAS system for improving working capital and cash conversion cycle.
### Strengths
- **Stronger strategic framework than EOS.** The 7 strata and OPSP force real strategic thinking.
- **Cash focus.** Unique among frameworks — explicitly addresses cash flow management.
- **Scales further.** Better suited for 100–1000 person companies than EOS.
- **Works for ambitious growth companies.** Designed for companies that want to scale significantly.
### Limitations
- **More complex than EOS.** Harder to DIY. Benefits heavily from a certified Scaling Up coach.
- **Overwhelming at first.** The full framework has many components. Teams often implement partially.
- **Less prescriptive on meetings.** EOS's L10 is very specific. Scaling Up's meeting rhythm requires more customization.
### Best fit
- Series A to Series C companies
- Companies with strong growth ambition
- Leadership teams that want strategic rigor, not just operational clarity
- Companies already past initial chaos, ready for more sophisticated frameworks
### Not ideal if
- You're pre-product-market-fit
- You need quick operational wins
- Your team doesn't have the bandwidth for the learning curve
---
## 3. OKR-Native (Google Style)
**Books:** *Measure What Matters* by John Doerr; *Radical Focus* by Christina Wodtke
### Core principles
OKRs = Objectives + Key Results
- **Objectives:** Qualitative, inspiring direction. "What are we trying to achieve?"
- **Key Results:** Quantitative, measurable outcomes. "How will we know we achieved it?"
- **Not tasks.** KRs measure outcomes, not activities.
**Cascade:** Company OKRs → Department OKRs → Team OKRs → Individual OKRs
**Cadence:** Quarterly OKR cycles. Weekly check-ins. Annual reflection.
**Scoring:** 0.0–1.0. Target is 0.7. Consistently hitting 1.0 = OKRs aren't ambitious enough.
### Strengths
- **Aligns the whole company.** When done well, every team can trace their work to company-level objectives.
- **Encourages ambition.** Moonshot OKRs are explicit. "Roofshot" vs "moonshot" OKRs.
- **Widely understood in tech.** Many hires will already know OKRs.
- **No framework cost.** No implementer required. Tooling is free or cheap (Linear, Notion, Lattice).
### Limitations
- **Hard to do well.** Most companies run "OKR theater" — tasks dressed up as key results.
- **Missing the HOW.** OKRs define what to achieve but not how to operate. You still need meeting rhythm, accountability structure, and issue resolution.
- **Misalignment risk.** If not cascaded properly, teams run disconnected OKRs that feel like alignment but aren't.
- **No operational backbone.** OKRs are a goal-setting system, not a full operating system.
### Best fit
- Tech companies with strong product/engineering culture
- Companies where hypothesis-driven work is already the norm
- Organizations that value autonomy and bottom-up goal setting
- As the goal-setting layer inside a broader operating system
### Not ideal if
- Teams lack discipline to hold each other accountable
- You need more than just goal alignment (issue resolution, meeting structure)
- Leaders don't model OKR behavior themselves
---
## 4. Holacracy
**Book:** *Holacracy* by Brian Robertson
### Core principles
Holacracy replaces the traditional management hierarchy with a system of distributed authority.
- **Circles:** Semi-autonomous units with defined purposes (like teams, but self-governing)
- **Roles:** People fill roles (not job descriptions). One person can hold multiple roles in different circles.
- **Governance meetings:** Roles and accountabilities are defined and evolved by the circle, not management
- **Tactical meetings:** Operational coordination within circles
- **The Constitution:** A legal document that all members ratify, replacing traditional management authority
### Strengths
- **Maximum autonomy.** People closest to the work define how it gets done.
- **Removes management as a bottleneck.** Decisions happen at the circle level.
- **Adapts to complexity.** Circle structure evolves organically as the work changes.
### Limitations
- **Enormous learning curve.** 2–4 years to full adoption. Many companies abandon it.
- **High meeting overhead.** Governance meetings add significant time.
- **Doesn't eliminate politics.** Just moves them to governance meetings.
- **Requires full commitment.** Partial Holacracy doesn't work. You either do it or you don't.
- **Not for crisis mode.** When speed matters, distributed governance slows you down.
### When it works
- Organizations with deep belief in autonomy and self-management
- Non-profit or mission-driven organizations where consensus matters
- Companies with patient leadership willing to invest years in implementation
### When it doesn't work
- Startups needing speed and clarity
- Companies with strong founder personalities who struggle to relinquish control
- Organizations that need to move fast or course-correct frequently
---
## 5. Custom Hybrid
### When to build a hybrid
None of the above frameworks fits perfectly because:
- EOS lacks strategic depth
- Scaling Up is complex to implement
- OKRs don't provide operational backbone
- Holacracy is too slow to implement
The solution: take the best components of each.
### Common hybrid patterns
**EOS backbone + OKR goal-setting:**
- EOS provides: accountability chart, L10 meeting, IDS, meeting pulse
- OKRs provide: goal-setting with ambition, cascade, and alignment checks
- Works well for: tech companies that want operational rigor with flexibility
**Scaling Up strategy + EOS execution:**
- Scaling Up provides: OPSP, 7 strata, cash management
- EOS provides: L10, rocks, IDS
- Works well for: ambitious growth companies that want both strategy and execution discipline
**OKRs + custom meeting rhythm:**
- OKRs provide: goal cascade
- Custom meetings: weekly team syncs, monthly department reviews, quarterly all-hands
- Works well for: companies that already have strong culture but need goal alignment
### Hybrid design principles
1. **Pick one goal-setting system.** Don't mix OKRs and Rocks — they're both 90-day priority systems and will create confusion.
2. **Be explicit about what you're taking from where.** "We use EOS for meetings and Scaling Up for strategy" is a clear hybrid. "We do a bit of everything" is chaos.
3. **Document your version.** Your operating system should have a name and a one-page description of what it includes.
4. **Evolve intentionally.** Change one component at a time. Don't overhaul the whole system when one part isn't working.
---
## Framework Selection Decision Tree
```
Is your company < 50 people and in operational chaos?
YES → Start with EOS. It's the simplest path to order.
NO → Continue.
Does strategic positioning and cash flow need significant work?
YES → Consider Scaling Up.
NO → Continue.
Is your company tech-native with strong product/engineering culture?
YES → OKR-native with a custom meeting rhythm.
NO → Continue.
Do you have 2+ years and full leadership commitment to radical organizational change?
YES → Consider Holacracy (with caution).
NO → Build a custom hybrid from EOS + OKRs.
```
FILE:competitive-intel/SKILL.md
---
name: "competitive-intel"
description: "Systematic competitor tracking that feeds CMO positioning, CRO battlecards, and CPO roadmap decisions. Use when analyzing competitors, building sales battlecards, tracking market moves, positioning against alternatives, or when user mentions competitive intelligence, competitive analysis, competitor research, battlecards, win/loss, or market positioning."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: competitive-strategy
updated: 2026-03-05
frameworks: ci-playbook, battlecard-template
---
# Competitive Intelligence
Systematic competitor tracking. Not obsession — intelligence that drives real decisions.
## Keywords
competitive intelligence, competitor analysis, battlecard, win/loss analysis, competitive positioning, competitive tracking, market intelligence, competitor research, SWOT, competitive map, feature gap analysis, competitive strategy
## Quick Start
```
/ci:landscape — Map your competitive space (direct, indirect, future)
/ci:battlecard [name] — Build a sales battlecard for a specific competitor
/ci:winloss — Analyze recent wins and losses by reason
/ci:update [name] — Track what a competitor did recently
/ci:map — Build competitive positioning map
```
## Framework: 5-Layer Intelligence System
### Layer 1: Competitor Identification
**Direct competitors:** Same ICP, same problem, comparable solution, similar price point.
**Indirect competitors:** Same budget, different solution (including "do nothing" and "build in-house").
**Future competitors:** Well-funded startups in adjacent space; large incumbents with stated roadmap overlap.
**The 2x2 Threat Matrix:**
| | Same ICP | Different ICP |
|---|---|---|
| **Same problem** | Direct threat | Adjacent (watch) |
| **Different problem** | Displacement risk | Ignore for now |
Update this quarterly. Who's moved quadrants?
### Layer 2: Tracking Dimensions
Track these 8 dimensions per competitor:
| Dimension | Sources | Cadence |
|-----------|---------|---------|
| **Product moves** | Changelog, G2/Capterra reviews, Twitter/LinkedIn | Monthly |
| **Pricing changes** | Pricing page, sales call intel, customer feedback | Triggered |
| **Funding** | Crunchbase, TechCrunch, LinkedIn | Triggered |
| **Hiring signals** | LinkedIn job postings, Indeed | Monthly |
| **Partnerships** | Press releases, co-marketing | Triggered |
| **Customer wins** | Case studies, review sites, LinkedIn | Monthly |
| **Customer losses** | Win/loss interviews, churned accounts | Ongoing |
| **Messaging shifts** | Homepage, ads (Facebook/Google Ad Library) | Quarterly |
### Layer 3: Analysis Frameworks
**SWOT per Competitor:**
- Strengths: What do they do well? Where do they win?
- Weaknesses: Where do they lose? What do customers complain about?
- Opportunities: What could they do that would threaten you?
- Threats: What's their existential risk?
**Competitive Positioning Map (2 axis):**
Choose axes that matter for your buyers:
- Common: Price vs Feature Depth; Enterprise-ready vs SMB-ready; Easy to implement vs Configurable
- Pick axes that show YOUR differentiation clearly
**Feature Gap Analysis:**
| Feature | You | Competitor A | Competitor B | Gap status |
|---------|-----|-------------|-------------|------------|
| [Feature] | ✅ | ✅ | ❌ | Your advantage |
| [Feature] | ❌ | ✅ | ✅ | Gap — roadmap? |
| [Feature] | ✅ | ❌ | ❌ | Moat |
| [Feature] | ❌ | ❌ | ✅ | Competitor B only |
### Layer 4: Output Formats
**For Sales (CRO):** Battlecards — one page per competitor, designed for pre-call prep.
See `templates/battlecard-template.md`
**For Marketing (CMO):** Positioning update — message shifts, new differentiators, claims to stop or start making.
**For Product (CPO):** Feature gap summary — what customers ask for that we don't have, what competitors ship, what to reprioritize.
**For CEO/Board:** Monthly competitive summary — 1-page: who moved, what it means, recommended responses.
### Layer 5: Intelligence Cadence
**Monthly (scheduled):**
- Review all tier-1 competitors (direct threats, top 3)
- Update battlecards with new intel
- Publish 1-page summary to leadership
**Triggered (event-based):**
- Competitor raises funding → assess implications within 48 hours
- Competitor launches major feature → product + sales response within 1 week
- Competitor poaches key customer → win/loss interview within 2 weeks
- Competitor changes pricing → analyze and respond within 1 week
**Quarterly:**
- Full competitive landscape review
- Update positioning map
- Refresh ICP competitive threat assessment
- Add/remove companies from tracking list
---
## Win/Loss Analysis
This is the highest-signal competitive data you have. Most companies do it too rarely.
**When to interview:**
- Every lost deal >$50K ACV
- Every churn >6 months tenure
- Every competitive win (learn why — it may not be what you think)
**Who conducts it:**
- NOT the AE who worked the deal (too close, prospect won't be candid)
- Customer success, product team, or external researcher
**Question structure:**
1. "Walk me through your evaluation process"
2. "Who else were you considering?"
3. "What were the top 3 criteria in your decision?"
4. "Where did [our product] fall short?"
5. "What was the deciding factor?"
6. "What would have changed your decision?"
**Aggregate findings monthly:**
- Win reasons (rank by frequency)
- Loss reasons (rank by frequency)
- Competitor win rates (by competitor, by segment)
- Patterns over time
---
## The Balance: Intelligence Without Obsession
**Signs you're over-tracking competitors:**
- Roadmap decisions are primarily driven by "they just shipped X"
- Team morale drops when competitors fundraise
- You're shipping features you don't believe in to match their checklist
- Pricing discussions always start with "well, they charge X"
**Signs you're under-tracking:**
- Your AEs get blindsided on calls
- Prospects know more about competitors than your team does
- You missed a major product launch until customers told you
- Your positioning hasn't changed in 12+ months despite market moves
**The right posture:**
- Know competitors well enough to win against them
- Don't let them set your agenda
- Your roadmap is led by customer problems, informed by competitive gaps
---
## Distributing Intelligence
| Audience | Format | Cadence | Owner |
|----------|--------|---------|-------|
| AEs + SDRs | Updated battlecards in CRM | Monthly + triggered | CRO |
| Product | Feature gap analysis | Quarterly | CPO |
| Marketing | Positioning brief | Quarterly | CMO |
| Leadership | 1-page competitive summary | Monthly | CEO/COO |
| Board | Competitive landscape slide | Quarterly | CEO |
**One source of truth:** All competitive intel lives in one place (Notion, Confluence, Salesforce). Avoid Slack-only distribution — it disappears.
---
## Red Flags in Competitive Intelligence
| Signal | What it means |
|--------|---------------|
| Competitor's win rate >50% in your core segment | Fundamental positioning problem, not sales problem |
| Same objection from 5+ deals: "competitor has X" | Feature gap that's real, not just optics |
| Competitor hired 10 engineers in your domain | Major product investment incoming |
| Competitor raised >$20M and targets your ICP | 12-month runway for them to compete hard |
| Prospects evaluate you to justify competitor decision | You're the "check box" — fix perception or segment |
## Integration with C-Suite Roles
| Intelligence Type | Feeds To | Output Format |
|------------------|----------|---------------|
| Product moves | CPO | Roadmap input, feature gap analysis |
| Pricing changes | CRO, CFO | Pricing response recommendations |
| Funding rounds | CEO, CFO | Strategic positioning update |
| Hiring signals | CHRO, CTO | Talent market intelligence |
| Customer wins/losses | CRO, CMO | Battlecard updates, positioning shifts |
| Marketing campaigns | CMO | Counter-positioning, channel intelligence |
## References
- `references/ci-playbook.md` — OSINT sources, win/loss framework, positioning map construction
- `templates/battlecard-template.md` — sales battlecard template
FILE:competitive-intel/references/ci-playbook.md
# Competitive Intelligence Playbook
## OSINT Sources for Competitor Tracking
### Free, Reliable Sources
**Company & Product:**
- **Their website** — pricing page (archive.org for history), product changelog, careers page
- **G2 / Capterra / Trustpilot** — customer reviews; filter by recency; read 1-star reviews carefully
- **LinkedIn** — job postings signal roadmap; company page for headcount trend; employees for leaks
- **GitHub** — open source activity; what they're building; engineering team size; tech stack
- **Crunchbase / PitchBook** (free tier) — funding history, investors, team changes
- **BuiltWith** — tech stack they use; signals about infrastructure maturity
**Messaging & Positioning:**
- **Facebook Ad Library** — see their current ad copy and creative; what messages they're testing
- **Google Keyword Planner** — which keywords they're bidding on
- **SEMrush / Ahrefs** (free trial or limited) — their organic keywords, backlink profile
- **Wayback Machine** — homepage evolution over time; when positioning shifted
- **Their blog** — content strategy reveals priorities and ICP assumptions
**News & Events:**
- **TechCrunch, VentureBeat** — funding announcements, major launches
- **Twitter/X / LinkedIn** — CEO + founders; direct signals about strategy
- **Podcast appearances** — founders talk more openly on podcasts than press releases
- **Job descriptions** — "Senior Engineer - Payments" means they're building payments
### Paid (Worth It for Tier-1 Competitors)
- **G2 Buyer Intent** — which prospects are researching your competitor right now
- **Bombora** — intent data for account-level research signals
- **PitchBook** — funding, investors, valuation estimates
- **Klue / Crayon / Kompyte** — dedicated CI platforms that aggregate automatically
### Primary Research (Best Signal)
- **Win/loss interviews** — the single highest-signal source (see below)
- **Talk to churned customers** — why did they switch? To whom?
- **Talk to their customers** — LinkedIn outreach; honest conversations
- **Industry events** — competitor presentations reveal roadmap; talk to attendees
- **Former employees** — LinkedIn; respectful outreach; no NDA violations
---
## Competitive Battlecard Format
A battlecard is a 1-page (or single screen) document for sales reps to reference before and during calls.
**Design principles:**
- Written for a rep with 2 minutes to prep, not a product manager
- Action-oriented: tells reps what to SAY, not just what to know
- Updated monthly at minimum; never more than 90 days old
### Battlecard Structure
```
COMPETITOR: [Name]
Last updated: [Date] | Owner: [Name]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
THE 30-SECOND SUMMARY
[One paragraph. Who they are, who they sell to, why they win.]
THEIR STRENGTHS (know these — don't dismiss them)
• [Strength 1] — what customers actually love about them
• [Strength 2]
• [Strength 3]
THEIR REAL WEAKNESSES (from win/loss data, not assumptions)
• [Weakness 1] — source: [customer quote / win/loss theme]
• [Weakness 2]
• [Weakness 3]
OUR DIFFERENTIATED ADVANTAGES
• [Advantage 1] — proof point: [metric/customer/case study]
• [Advantage 2] — proof point:
• [Advantage 3] — proof point:
COMMON OBJECTIONS + RESPONSES
"They have [feature] and you don't."
→ [Response. Acknowledge, reframe, redirect.]
"They're cheaper."
→ [Response with ROI angle or TCO comparison.]
"They're more established / bigger."
→ [Response. Size isn't always advantage; use to your benefit.]
TRAP-SETTING QUESTIONS (ask these early to shift the eval criteria)
• "How important is [your differentiator] to your team?"
• "Have you looked at [pain point they create]?"
• "What happens to your workflow when [their known limitation occurs]?"
WHEN WE WIN
• [Segment or scenario where we almost always beat them]
• [Use case where we're clearly stronger]
WHEN WE LOSE (be honest)
• [Scenario where they're genuinely better — don't fight these battles]
• [Segment where they have structural advantages]
DO NOT SAY
• Don't claim [X] — it's not true and they'll call it out
• Don't say [Y] — prospect will already know it and it sounds desperate
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
---
## Win/Loss Analysis Framework
### Why Most Companies Do This Wrong
- They survey instead of interview (surveys get polite answers)
- The AE conducts it (too emotionally invested; prospect won't be candid)
- They do it 6 months after the decision (memory fades)
- They look for confirmation of what they believe
### The Right Process
**Timing:** Within 30 days of deal closed/lost/churned.
**Interviewer:** Customer success, product, or external researcher. Never the AE.
**Duration:** 30 minutes (budget 45).
**Incentive:** $100 gift card gets you 80% acceptance. Worth it.
**Interview Guide:**
*Opening:*
"I'm [name] from [company]. I'm not in sales — I'm trying to understand what drove your decision so we can improve. There's nothing you can say that will change the outcome. I just want honest feedback."
*Core questions:*
1. "Can you walk me through your evaluation process from the beginning?"
2. "Who were the key stakeholders involved in the decision?"
3. "What were the 3 most important criteria you were evaluating against?"
4. "Which vendors did you seriously consider?"
5. "Where did [company] fall short of your expectations?" (For losses)
OR "What tipped the decision in [company]'s favor?" (For wins)
6. "Was price a factor? How significant?"
7. "What would have had to be different for you to choose [us / the other option]?"
8. "Any advice for our team on how we handled the process?"
**Data aggregation:**
- Tag every response: [criterion], [competitor mentioned], [product gap], [sales process], [price], [trust/credibility]
- Monthly rollup: top 5 win reasons, top 5 loss reasons, competitor win rate
- Share with: CEO, CRO, CPO, CMO — not just sales
---
## Competitive Positioning Map Construction
A positioning map shows where you sit relative to competitors on 2 dimensions that BUYERS care about.
### Step 1: Choose Your Axes
- Pick dimensions that actually drive purchase decisions in your segment
- At least one axis should be where you win
- Avoid generic axes ("feature-rich vs. simple" tells you nothing)
**Good axis pairs:**
- Implementation time (days vs. months) × Customization depth
- Price point × Enterprise readiness
- Automation level × Human-in-the-loop control
- Time-to-value × Total cost of ownership
**Bad axes:**
- Quality (too vague)
- "Innovation" (unmeasurable)
- Any axis where all competitors cluster in the same spot
### Step 2: Place Competitors Objectively
- Use customer quotes and win/loss data to justify placement
- Don't place competitors where you WANT them — where they ACTUALLY are
- If you're unsure, ask 5 customers to place them
### Step 3: Find and Name Your White Space
- Where is there a position no competitor holds?
- Is that white space there because it's valuable (opportunity) or worthless (avoid)?
- Can you credibly occupy it?
### Step 4: Test Your Positioning
- Show the map to 5 prospects: "Does this match your perception?"
- Show it to 5 lost prospects: "Where would you place [the winner] and us?"
- Adjust until map matches buyer reality, not internal perception
---
## Intelligence Sharing Across Roles
### What Each Role Needs and When
**CRO (Sales):**
- Needs: Battlecards, win rates by competitor, competitor objections + responses
- Cadence: Updated battlecards monthly; triggered updates on major competitor moves
- Format: 1-pager per competitor in CRM, linked from deal record
**CMO (Marketing):**
- Needs: Messaging shifts, new claims, ad spend signals, keyword battles
- Cadence: Quarterly positioning review, triggered on major launches
- Format: Positioning brief with recommended response to messaging shifts
**CPO (Product):**
- Needs: Feature gap analysis, competitor roadmap signals (job postings, changelog), what we lose to
- Cadence: Monthly feature gap update, triggered on major launches
- Format: Feature comparison matrix + gap prioritization recommendation
**CTO (Engineering):**
- Needs: Tech stack signals, infrastructure approaches, scale they've achieved
- Cadence: Quarterly
- Format: Technical comparison notes, relevant for architectural decisions
**CEO:**
- Needs: Summary of threat landscape, recommended responses, board-level narrative
- Cadence: Monthly 1-pager + quarterly deep dive
- Format: 1-page brief: who moved, what it means, what we do
### The Single Source of Truth Rule
All competitive intel in one place. Suggest:
- Notion database per competitor: profile, battlecard, changelog, win/loss notes
- Slack channel: `#competitive-intel` for real-time triggered alerts
- Monthly digest email to leadership
If it lives only in Slack, it disappears. If it lives only in a wiki that nobody reads, it doesn't matter. Combine both.
---
## How to Track Without Obsessing
**Set up the system, then let it run:**
- Google Alerts for competitor names + CEO names
- LinkedIn Saved Searches for their job postings
- Klue/Crayon if budget allows (automated aggregation)
- Monthly 60-minute competitive review meeting (not 4 hours)
**What to do when competitor makes a big move:**
1. Read the announcement objectively
2. Talk to 3 customers: "Did you see this? What do you think?"
3. Assess: does this change any buying criteria in your deals?
4. If yes: update battlecard and positioning within 1 week
5. If no: log it, move on
**The test:** After reviewing a competitor move, do you feel urgency to ship something? If yes, you're reacting. The right feeling is "noted — let's see if customers care."
FILE:competitive-intel/templates/battlecard-template.md
# Sales Battlecard Template
**COMPETITOR:** [Name]
**Last updated:** [YYYY-MM-DD] | **Owner:** [Name]
**Win rate vs this competitor:** [X]% | **Deals tracked:** [N]
---
## 30-Second Summary
[Who they are. Who they target. Why they win. What they're known for. 3-4 sentences max.]
---
## Their Strengths
*Know these. Don't dismiss them. Prospects have already heard their pitch.*
- **[Strength]:** [What customers genuinely love; source if available]
- **[Strength]:** [Specific capability or trait]
- **[Strength]:** [Brand, market position, or ecosystem advantage]
---
## Their Real Weaknesses
*From win/loss data only — not wishful thinking.*
- **[Weakness]:** "[Customer quote]" — seen in [N] deals
- **[Weakness]:** [Documented limitation with evidence]
- **[Weakness]:** [Implementation, support, or pricing issue]
---
## Our Differentiated Advantages
*Must be real and provable. Each needs a proof point.*
- **[Advantage]:** [Proof: metric / customer quote / case study]
- **[Advantage]:** [Proof]
- **[Advantage]:** [Proof]
---
## Common Objections + Responses
**"They have [feature X] and you don't."**
> [Acknowledge. Reframe to your strength. Redirect to outcome.
> "You're right that they have X. What we've found is that customers who care most about X tend to also care about [Y], where we're significantly stronger. Can I show you [specific example]?"]
**"They're cheaper."**
> [Don't fight on price. Reframe to TCO or ROI.
> "They are lower in initial cost. Most customers find the total cost over 12 months is actually comparable when you factor in [implementation time / support costs / integrations]. Want to walk through that?"]
**"They've been around longer / they're more established."**
> [Reframe tenure as potential liability or irrelevance.
> "Their longevity means they have a lot of technical debt and a big customer base that pulls their roadmap in every direction. Our customers tell us that's exactly why they chose us — we move faster and we're laser-focused on [their specific use case]."]
**"[Competitor] is already used by [big customer they respect]."**
> [Name-drop your wins in their segment.
> "We work with [comparable logo]. Want me to connect you with their [role] to ask how they made the decision?"]
---
## Trap-Setting Questions
*Ask early in discovery to establish criteria that favor you.*
- "How important is [your key differentiator] to your workflow?"
- "What happens when [their known limitation] occurs? Has that been an issue before?"
- "How long does your team typically take to onboard a new tool?"
- "Who manages the integration work — do you have dedicated engineering resources for that?"
- "What does your current vendor do when you need support?"
---
## When We Win
- [Scenario or segment where we consistently beat them]
- [Use case that plays to our strengths]
- [Buyer profile that prefers us]
## When We Lose (Be Honest)
- [Scenario where they genuinely win — don't fight here]
- [Segment where their strengths matter more than ours]
---
## Do NOT Say
- ❌ Don't claim [X] — it's not accurate and they'll check
- ❌ Don't attack [Y] — it backfires and makes us look insecure
- ❌ Don't say "we're better" without specifics — be concrete
---
## Recent Intel
*Last 90 days only. Older than 90 days: archive.*
- [Date]: [What happened — funding, product launch, pricing change, key hire]
- [Date]: [Customer feedback from win/loss interview]
- [Date]: [Any notable market move]
---
*Battlecards are only useful if current. If this is >90 days old, flag to [owner] for update.*
FILE:context-engine/SKILL.md
---
name: "context-engine"
description: "Loads and manages company context for all C-suite advisor skills. Reads ~/.claude/company-context.md, detects stale context (>90 days), enriches context during conversations, and enforces privacy/anonymization rules before external API calls."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: orchestration
updated: 2026-03-05
frameworks: context-loading, anonymization, context-enrichment
---
# Company Context Engine
The memory layer for C-suite advisors. Every advisor skill loads this first. Context is what turns generic advice into specific insight.
## Keywords
company context, context loading, context engine, company profile, advisor context, stale context, context refresh, privacy, anonymization
---
## Load Protocol (Run at Start of Every C-Suite Session)
**Step 1 — Check for context file:** `~/.claude/company-context.md`
- Exists → proceed to Step 2
- Missing → prompt: *"Run /cs:setup to build your company context — it makes every advisor conversation significantly more useful."*
**Step 2 — Check staleness:** Read `Last updated` field.
- **< 90 days:** Load and proceed.
- **≥ 90 days:** Prompt: *"Your context is [N] days old. Quick 15-min refresh (/cs:update), or continue with what I have?"*
- If continue: load with `[STALE — last updated DATE]` noted internally.
**Step 3 — Parse into working memory.** Always active:
- Company stage (pre-PMF / scaling / optimizing)
- Founder archetype (product / sales / technical / operator)
- Current #1 challenge
- Runway (as risk signal — never share externally)
- Team size
- Unfair advantage
- 12-month target
---
## Context Quality Signals
| Condition | Confidence | Action |
|-----------|-----------|--------|
| < 30 days, full interview | High | Use directly |
| 30–90 days, update done | Medium | Use, flag what may have changed |
| > 90 days | Low | Flag stale, prompt refresh |
| Key fields missing | Low | Ask in-session |
| No file | None | Prompt /cs:setup |
If Low: *"My context is [stale/incomplete] — I'm assuming [X]. Correct me if I'm wrong."*
---
## Context Enrichment
During conversations, you'll learn things not in the file. Capture them.
**Triggers:** New number or timeline revealed, key person mentioned, priority shift, constraint surfaces.
**Protocol:**
1. Note internally: `[CONTEXT UPDATE: {what was learned}]`
2. At session end: *"I picked up a few things to add to your context. Want me to update the file?"*
3. If yes: append to the relevant dimension, update timestamp.
**Never silently overwrite.** Always confirm before modifying the context file.
---
## Privacy Rules
### Never send externally
- Specific revenue or burn figures
- Customer names
- Employee names (unless publicly known)
- Investor names (unless public)
- Specific runway months
- Watch List contents
### Safe to use externally (with anonymization)
- Stage label
- Team size ranges (1–10, 10–50, 50–200+)
- Industry vertical
- Challenge category
- Market position descriptor
### Before any external API call or web search
Apply `references/anonymization-protocol.md`:
- Numbers → ranges or stage-relative descriptors
- Names → roles
- Revenue → percentages or stage labels
- Customers → "Customer A, B, C"
---
## Missing or Partial Context
Handle gracefully — never block the conversation.
- **Missing stage:** "Just to calibrate — are you still finding PMF or scaling what works?"
- **Missing financials:** Use stage + team size to infer. Note the gap.
- **Missing founder profile:** Infer from conversation style. Mark as inferred.
- **Multiple founders:** Context reflects the interviewee. Note co-founder perspective may differ.
---
## Required Context Fields
```
Required:
- Last updated (date)
- Company Identity → What we do
- Stage & Scale → Stage
- Founder Profile → Founder archetype
- Current Challenges → Priority #1
- Goals & Ambition → 12-month target
High-value optional:
- Unfair advantage
- Kill-shot risk
- Avoided decision
- Watch list
```
Missing required fields: note gaps, work around in session, ask in-session only when critical.
---
## References
- `references/anonymization-protocol.md` — detailed rules for stripping sensitive data before external calls
FILE:context-engine/references/anonymization-protocol.md
# Anonymization Protocol
Rules for stripping sensitive company data before any external API call, web search, or tool invocation that sends data outside the local environment.
---
## When This Protocol Applies
**Trigger:** Any time company context or conversation content will leave the local session.
Examples:
- Web search that includes company specifics
- External API call with company data in the payload
- Any tool call where conversation content is part of the request
**Does NOT apply to:**
- Local file reads/writes (`~/.claude/company-context.md`)
- In-session reasoning and analysis
- Generating advice or documents that stay local
---
## Rule 1: Financial Figures → Relative Ranges
Never send specific financial data externally.
| Raw data | Anonymized version |
|----------|-------------------|
| "$2.4M ARR" | "early-stage ARR (sub-$5M)" |
| "$180K MRR" | "growing MRR, Series A range" |
| "14 months runway" | "runway is healthy for stage" |
| "burn rate is $320K/month" | "burn rate is moderate for stage" |
| "raised $8M Series A" | "Series A company" |
| "customer LTV is $4,200" | "LTV is above industry average for segment" |
| "CAC is $680" | "CAC is in a sustainable range" |
**Rule:** No dollar amounts. No month counts for runway. Use stage-relative descriptors.
---
## Rule 2: Customer Names → Anonymized Labels
Never send customer or client names externally.
| Raw data | Anonymized version |
|----------|-------------------|
| "Acme Corp is our biggest customer" | "Customer A (largest account)" |
| "we're working with NHS England" | "a large public-sector customer" |
| "BMW, Volkswagen, and Stellantis" | "three major automotive OEMs" |
| "10 enterprise customers including..." | "10 enterprise customers" |
**Rule:** Use "Customer A/B/C" for named accounts, or describe by segment without naming.
---
## Rule 3: Revenue Figures → Percentage Changes or Stage Descriptors
Revenue trajectory is safer than absolute numbers.
| Raw data | Anonymized version |
|----------|-------------------|
| "growing from $1M to $2M ARR" | "2x revenue growth year-over-year" |
| "revenue dropped from $500K to $430K" | "revenue declined ~15% in the period" |
| "hit $10M ARR last quarter" | "crossed a significant ARR milestone" |
| "doing $50K MRR" | "pre-Series A revenue, strong growth trajectory" |
**Rule:** Percentages and directional signals (growing / declining / flat) are safe. Absolutes are not.
---
## Rule 4: Employee Names → Roles Only
Never send individual names externally.
| Raw data | Anonymized version |
|----------|-------------------|
| "Our CTO, Sarah Chen, is struggling" | "our CTO is struggling with the transition" |
| "James is the best performer on the team" | "our strongest performer is in the engineering lead role" |
| "we're about to let go of Michael" | "we're about to make a leadership change" |
| "the founding team is me, Alex, and Priya" | "a three-person founding team" |
**Exception:** Publicly known executives (CEO of a public company, named in press releases) can be referenced by name. If in doubt, use role.
---
## Rule 5: Investor Names → Generic Descriptors
| Raw data | Anonymized version |
|----------|-------------------|
| "Sequoia led our round" | "a top-tier VC led our round" |
| "our lead investor is pushing for an exit" | "pressure from investors toward exit" |
| "Y Combinator alumni" | "accelerator alumni" |
**Exception:** YC, Techstars, and similar well-known accelerators are commonly referenced and safe if the founder has publicly disclosed. When in doubt, omit.
---
## Rule 6: Location → Country or Region
| Raw data | Anonymized version |
|----------|-------------------|
| "Berlin-based startup" | "European startup" |
| "we're in San Francisco" | "US-based startup" |
| "expanding to Munich and Vienna" | "expanding in the DACH region" |
**Exception:** Location is less sensitive than financials. Use judgment — if it's on their website, it's fine.
---
## Anonymization Decision Tree
```
Before sending data externally:
1. Does it include a specific dollar amount?
→ YES: Replace with range or relative descriptor
2. Does it include a person's name?
→ YES: Replace with role only (unless publicly known)
3. Does it include a company or customer name?
→ YES: Replace with "Customer A" or segment descriptor
4. Does it include specific headcount or runway months?
→ YES: Replace with range (1–10, 10–50) or "healthy/tight/critical"
5. Does it include proprietary data, roadmap, or unreleased product info?
→ YES: Do not include. Reference only generically ("product expansion planned")
6. Is it publicly available information?
→ YES: Safe to send as-is
```
---
## Required vs Optional Anonymization
### Required (always strip before external calls)
- Revenue figures (absolute)
- Burn rate (absolute)
- Runway (specific months)
- Customer names
- Employee names
- Investor names (unless public)
- Funding amounts (unless public)
### Optional (use judgment based on sensitivity)
- Industry vertical (usually fine)
- Company stage (usually fine)
- Team size ranges (usually fine)
- Geographic region (usually fine)
- General challenge category (usually fine)
---
## What to Do If You're Unsure
Default to stricter anonymization. The cost of over-anonymizing is slightly less useful external results. The cost of under-anonymizing is a privacy breach.
When in doubt: **remove it**.
---
## Audit Log (Internal Only)
When running external calls with company context, note internally:
```
[EXTERNAL CALL: {tool/API used}]
[ANONYMIZED: {fields stripped}]
[RETAINED: {fields kept and why}]
```
This is for internal reasoning only — never included in output to the founder.
FILE:coo-advisor/SKILL.md
---
name: "coo-advisor"
description: "Operations leadership for scaling companies. Process design, OKR execution, operational cadence, and scaling playbooks. Use when designing operations, setting up OKRs, building processes, scaling teams, analyzing bottlenecks, planning operational cadence, or when user mentions COO, operations, process improvement, OKRs, scaling, operational efficiency, or execution."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: coo-leadership
updated: 2026-03-05
python-tools: ops_efficiency_analyzer.py, okr_tracker.py
frameworks: scaling-playbook, ops-cadence, process-frameworks
---
# COO Advisor
Operational frameworks and tools for turning strategy into execution, scaling processes, and building the organizational engine.
## Keywords
COO, chief operating officer, operations, operational excellence, process improvement, OKRs, objectives and key results, scaling, operational efficiency, execution, bottleneck analysis, process design, operational cadence, meeting cadence, org scaling, lean operations, continuous improvement
## Quick Start
```bash
python scripts/ops_efficiency_analyzer.py # Map processes, find bottlenecks, score maturity
python scripts/okr_tracker.py # Cascade OKRs, track progress, flag at-risk items
```
## Core Responsibilities
### 1. Strategy Execution
The CEO sets direction. The COO makes it happen. Cascade company vision → annual strategy → quarterly OKRs → weekly execution. See `references/ops_cadence.md` for full OKR cascade framework.
### 2. Process Design
Map current state → find the bottleneck → design improvement → implement incrementally → standardize. See `references/process_frameworks.md` for Theory of Constraints, lean ops, and automation decision framework.
**Process Maturity Scale:**
| Level | Name | Signal |
|-------|------|--------|
| 1 | Ad hoc | Different every time |
| 2 | Defined | Written but not followed |
| 3 | Measured | KPIs tracked |
| 4 | Managed | Data-driven improvement |
| 5 | Optimized | Continuous improvement loops |
### 3. Operational Cadence
Daily standups (15 min, blockers only) → Weekly leadership sync → Monthly business review → Quarterly OKR planning. See `references/ops_cadence.md` for full templates.
### 4. Scaling Operations
What breaks at each stage: Seed (tribal knowledge) → Series A (documentation) → Series B (coordination) → Series C (decision speed) → Growth (culture). See `references/scaling_playbook.md` for detailed playbook per stage.
### 5. Cross-Functional Coordination
RACI for key decisions. Escalation framework: Team lead → Dept head → COO → CEO based on impact scope.
## Key Questions a COO Asks
- "What's the bottleneck? Not what's annoying — what limits throughput."
- "How many manual steps? Which break at 3x volume?"
- "Who's the single point of failure?"
- "Can every team articulate how their work connects to company goals?"
- "The same blocker appeared 3 weeks in a row. Why isn't it fixed?"
## Operational Metrics
| Category | Metric | Target |
|----------|--------|--------|
| Execution | OKR progress (% on track) | > 70% |
| Execution | Quarterly goals hit rate | > 80% |
| Speed | Decision cycle time | < 48 hours |
| Quality | Customer-facing incidents | < 2/month |
| Efficiency | Revenue per employee | Track trend |
| Efficiency | Burn multiple | < 2x |
| People | Regrettable attrition | < 10% |
## Red Flags
- OKRs consistently 1.0 (not ambitious) or < 0.3 (disconnected from reality)
- Teams can't explain how their work maps to company goals
- Leadership meetings produce no action items two weeks running
- Same blocker in three consecutive syncs
- Process exists but nobody follows it
- Departments optimize local metrics at expense of company metrics
## Integration with Other C-Suite Roles
| When... | COO works with... | To... |
|---------|-------------------|-------|
| Strategy shifts | CEO | Translate direction into ops plan |
| Roadmap changes | CPO + CTO | Assess operational impact |
| Revenue targets change | CRO | Adjust capacity planning |
| Budget constraints | CFO | Find efficiency gains |
| Hiring plans | CHRO | Align headcount with ops needs |
| Security incidents | CISO | Coordinate response |
## Detailed References
- `references/scaling_playbook.md` — what changes at each growth stage
- `references/ops_cadence.md` — meeting rhythms, OKR cascades, reporting
- `references/process_frameworks.md` — lean ops, TOC, automation decisions
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- Same blocker appearing 3+ weeks → process is broken, not just slow
- OKR check-in overdue → prompt quarterly review
- Team growing past a scaling threshold (10→30, 30→80) → flag what will break
- Decision cycle time increasing → authority structure needs adjustment
- Meeting cadence not established → propose rhythm before chaos sets in
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Set up OKRs" | Cascaded OKR framework (company → dept → team) |
| "We're scaling fast" | Scaling readiness report with what breaks next |
| "Our process is broken" | Process map with bottleneck identified + fix plan |
| "How efficient are we?" | Ops efficiency scorecard with maturity ratings |
| "Design our meeting cadence" | Full cadence template (daily → quarterly) |
## Reasoning Technique: Step by Step
Map processes sequentially. Identify each step, handoff, and decision point. Find the bottleneck using throughput analysis. Propose improvements one step at a time.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:coo-advisor/references/ops_cadence.md
# Operational Cadence: Meetings, Async, Decisions, and Reporting
> The rhythm of your company determines its output. Bad cadence = constant context-switching, decisions made without information, and a leadership team that's always reactive.
---
## Philosophy
**Meetings are a tax.** Every hour in a meeting is an hour not spent building, selling, or serving customers. A good cadence minimizes meeting time while ensuring the right people have the right information at the right time.
**Async is default, sync is exception.** Most information sharing and routine updates should happen in writing. Reserve synchronous time for things that genuinely require real-time discussion: decisions with significant disagreement, complex problem-solving, relationship-building.
**Cadence serves strategy.** The calendar reflects priorities. If you're doing monthly all-hands but weekly status updates, you've inverted the importance.
---
## Meeting Cadence Templates
### Daily Operations
#### Daily Standup (Engineering / Product Teams)
**Format:** Async-first (Slack/Loom); sync only if blocked
**Sync duration:** 15 minutes max
**Participants:** Team (5–10 people)
**Facilitator:** Team lead or rotating
```
ASYNC FORMAT (post in #standup channel):
Yesterday: [What I completed]
Today: [What I'm working on]
Blocked: [Anything blocking me — tag the person who can unblock]
```
**Rules:**
- No status reporting in sync standup if everyone can read the async update
- Standups are not problem-solving sessions — take issues offline
- Skip standup if the team has a full-team session that day
- Kill standup if the team consistently has nothing blocked; replace with async
#### Daily Leadership Check-in (COO)
**Format:** Async only — read, don't meet
**Time:** 8:00–8:30 AM
**COO morning read:**
1. Yesterday's key metrics dashboard (5 min)
2. Overnight Slack/email escalations (5 min)
3. Today's decisions needed list (5 min)
4. Any P0/P1 incidents (check status page + on-call logs)
---
### Weekly Cadence
#### Leadership Sync (Weekly)
**Duration:** 60–90 minutes
**Participants:** C-suite + VP level
**Owner:** COO (or CEO)
**Day/Time:** Monday or Tuesday, morning
```
AGENDA TEMPLATE:
00:00–10:00 Metrics pulse (pre-read required — no presenting charts)
- Revenue: ACV, pipeline, churn delta
- Product: shipped last week, blockers this week
- Engineering: incidents, velocity
- CS: escalations, NPS delta
- People: open reqs, attrition flag
10:00–45:00 Priority items (submitted in advance, max 3)
- Item 1: [Owner: Name] [Decision needed / FYI / Input needed]
- Item 2: [Owner: Name]
- Item 3: [Owner: Name]
45:00–60:00 Parking lot / open
- Anything not covered
- Next week flagging
```
**Pre-meeting requirements:**
- Metrics dashboard updated by EOD Friday
- Priority items submitted by Sunday 6 PM
- Anyone who hasn't read the pre-read gets no floor time
**Output:** Decision log updated with outcomes, action items assigned in tracking system
#### 1:1 (Manager ↔ Direct Report)
**Duration:** 30–45 minutes
**Frequency:** Weekly (skip-levels: bi-weekly)
**Owner:** Report (the direct report sets agenda)
```
1:1 STRUCTURE:
[5 min] What's on your mind / temperature check
[15 min] Their agenda — what they want to discuss
[10 min] Manager agenda — feedback, context, decisions
[5 min] Action items review from last week
```
**1:1 anti-patterns to eliminate:**
- Using 1:1 for status updates (that's what standups are for)
- Manager dominating the agenda
- Skipping because "things are fine"
- No written record of what was discussed
**Private 1:1 doc:** Every manager/report pair maintains a shared doc with running notes, action items, and career development thread.
#### Cross-Functional Weekly Sync
**Duration:** 45 minutes
**Participants:** 2–4 team leads with shared dependencies
**Examples:** Product + Engineering, Sales + CS, Marketing + Sales
```
AGENDA:
00–10 Shared metrics (things both teams care about)
10–30 Active collaboration items — what needs coordination this week
30–40 Blockers + dependencies (what do I need from your team?)
40–45 Upcoming: what's coming that the other team should know about
```
---
### Monthly Cadence
#### All-Hands / Town Hall
**Duration:** 60–90 minutes
**Participants:** Entire company
**Owner:** CEO + functional heads
**Format:** In-person preferred; video if distributed
```
ALL-HANDS AGENDA (60 min version):
00–05 Opening — CEO sets the tone
05–20 Business update
- Where we are vs. plan (actuals vs. budget)
- Key wins and learning moments from last month
- What we're focused on this month
20–40 Functional spotlights (2 functions, 10 min each)
- What we shipped / what we did
- What we learned
- What's next
40–55 Open Q&A (no screened questions — take everything)
55–60 Closing
ALL-HANDS PREP CHECKLIST:
□ CEO talking points reviewed 48h in advance
□ Metrics slides reviewed by Finance for accuracy
□ Q&A prep — leadership team briefs on likely questions
□ Recording setup confirmed
□ Async option for timezones (recording posted within 2h)
□ Action items from Q&A captured and published within 24h
```
#### Monthly Business Review (MBR)
**Duration:** 2 hours
**Participants:** Leadership team
**Owner:** COO
```
MBR AGENDA:
00–20 Financial review (Finance presents)
- Revenue vs. plan, by segment
- Burn rate, runway
- Headcount actual vs. plan
- Key cost drivers
20–60 Functional reviews (each VP, 8 min each)
Standard template per function:
- Metrics: [3 key metrics vs. prior month vs. plan]
- Wins: [top 2-3 wins]
- Gaps: [where we missed and why]
- Next 30 days: [top 3 priorities]
60–90 Strategic topics (pre-submitted)
- Items requiring cross-functional decision
- Risks or issues needing leadership visibility
90–110 Decisions and action items
- Document decisions made
- Assign owners and deadlines
110–120 Retrospective
- What's working in how we operate?
- What needs to change?
```
**MBR pre-read package** (published 48h before):
- Financial summary (1 page)
- Each function's 1-pager (see template below)
```
FUNCTIONAL 1-PAGER TEMPLATE:
Function: [Name] Month: [Month Year]
Owner: [VP Name]
TOP METRICS:
| Metric | Target | Actual | vs. LM | vs. Plan |
|--------|--------|--------|--------|----------|
| [M1] | | | | |
| [M2] | | | | |
| [M3] | | | | |
WINS (2-3 bullets):
•
•
GAPS (be honest — no spin):
•
•
DEPENDENCIES (what I need from other teams):
•
NEXT 30 DAYS (top 3 priorities):
1.
2.
3.
```
---
### Quarterly Cadence
#### Quarterly Business Review (QBR)
**Duration:** Half day (4 hours)
**Participants:** Leadership team + key functional leads
**Owner:** CEO + COO
```
QBR AGENDA (4 hours):
PART 1: Look back (90 min)
- CEO: Business context and narrative (15 min)
- Finance: Full quarter P&L review (20 min)
- Each function: 10-min review against OKRs
Format: Hit/Miss/Partial for each objective + root cause
PART 2: Look forward (90 min)
- Product/Engineering: What ships next quarter (20 min)
- Sales/Marketing: Pipeline and demand plan (20 min)
- People: Headcount plan and key hires (15 min)
- Finance: Budget and forecast (20 min)
- Cross-functional dependencies (15 min)
PART 3: Strategic discussion (60 min)
- 1–2 strategic topics requiring deep discussion
- Pre-submitted and pre-read
PART 4: OKR setting for next quarter (30 min)
- Draft OKRs reviewed and challenged
- Final OKRs locked or assigned for next week finalization
```
#### Quarterly Leadership Off-site
**Duration:** 1–2 days (Series B+)
**Participants:** C-suite + VPs
**Purpose:** Strategy alignment, relationship building, hard conversations
**Off-site agenda principles:**
- No laptops during sessions (phones away)
- At least 50% discussion, max 50% presentation
- Include one session on how the leadership team is functioning (not just what the business is doing)
- Output: 1-page summary of decisions and commitments shared with the company
---
### Annual Cadence
#### Annual Planning Cycle
**Timeline:** Start 8–10 weeks before fiscal year end
```
ANNUAL PLANNING TIMELINE:
Week -10: Company strategic priorities draft (CEO + COO)
Week -8: Revenue model + market analysis (Finance + Sales)
Week -7: Functional goal-setting begins
Week -6: Headcount planning by function
Week -5: Draft plans reviewed by COO
Week -4: Cross-functional dependency alignment
Week -3: Budget finalization
Week -2: Board review (if applicable)
Week -1: Final company OKRs published
Week 0: Year kick-off all-hands
```
#### Year Kick-off All-Hands
**Duration:** 2–4 hours
**Participants:** Entire company
**Purpose:** Align entire company on year strategy and goals
```
KICK-OFF AGENDA:
- Last year retrospective: What we accomplished, what we learned
- Market context: Why now, why us
- Year strategy: The 2-3 things that matter most
- OKRs: Company-level goals, each function's goals
- Culture: How we'll work together
- Q&A: Open and honest
```
---
## Async Communication Frameworks
### The Writing-First Culture
All communication defaults to written unless real-time is genuinely necessary. This is how you scale decision-making without scaling meetings.
**Written first means:**
- Decisions are documented before they're communicated
- Updates are published before questions are asked
- Problems are described before solutions are proposed
### Slack Channel Architecture
```
REQUIRED CHANNELS:
#announcements Read-only. Major company announcements only.
#general Company-wide conversation
#leadership-public Leadership decisions visible to all (transparency)
#incidents P0/P1 incidents only. Auto-resolved when incident is closed.
#metrics Automated metric updates. No discussion here.
#wins Customer wins, team wins. Culture channel.
FUNCTIONAL CHANNELS:
#engineering, #product, #sales, #marketing, #cs, #people, #finance
PROJECT CHANNELS:
#proj-[name] Temporary. Archive when project ships.
DECISION CHANNELS:
#decisions All cross-team decisions logged here with context
```
**Anti-patterns to eliminate:**
- DMs for work decisions (decisions belong in channels, visible to team)
- @channel abuse (train people — this means everyone stops what they're doing)
- Thread avoidance (all replies go in threads, period)
- Multiple channels for same function (merge aggressively)
### Async Decision Template
When a decision needs input but doesn't require a meeting:
```
DECISION REQUEST (post in #decisions):
**Context:** [1-3 sentences on why this decision is needed]
**Options considered:**
A) [Option A] — Pros: X. Cons: Y.
B) [Option B] — Pros: X. Cons: Y.
**Recommendation:** [Your recommendation and why]
**Input needed from:** @person1, @person2 (tag specific people)
**Decide by:** [Date/Time — give at least 24 hours]
**If no response:** [Default action if no input received]
```
### Loom / Video for Async Communication
Use async video for:
- Explaining complex technical architecture
- Walking through a design or document with context
- Giving feedback that needs tone/nuance
- Team updates that would otherwise be a meeting
**Loom best practices:**
- Keep under 5 minutes; break up anything longer
- Always include a summary comment with key points
- Ask viewers to leave timestamp comments for specific questions
---
## Decision-Making Frameworks
### RAPID
The most practical decision-making framework for startups scaling to enterprises.
| Role | Meaning | Responsibility |
|------|---------|---------------|
| **R** — Recommend | Proposes decision with analysis | Does the work, gathers input, makes recommendation |
| **A** — Agree | Must agree before decision is final | Has veto power; should be used sparingly |
| **P** — Perform | Executes the decision | Consulted during recommendation phase |
| **I** — Input | Consulted for perspective | Shares point of view; not binding |
| **D** — Decide | Makes the final call | One person only — groups don't decide |
**How to use RAPID:**
1. For every significant decision, explicitly assign R, A, P, I, D before work begins
2. The D role is always one person — never a committee
3. Agree (A) roles should be limited to 2–3 people maximum; more = paralysis
4. Post the RAPID in the decision doc so everyone knows the structure
**Example application:**
```
Decision: Migrate from PostgreSQL to distributed database
R: VP Engineering
A: CTO, COO (for cost implications)
P: Infrastructure team
I: Product leads, Finance
D: CTO
```
### RACI
Better for ongoing processes than one-time decisions. Use RACI for recurring operational responsibilities.
| Role | Meaning |
|------|---------|
| **R** — Responsible | Does the work |
| **A** — Accountable | Owns the outcome; one person only |
| **C** — Consulted | Input before decisions/actions |
| **I** — Informed | Told of decisions/actions after the fact |
**RACI matrix template:**
```
PROCESS: Customer Escalation Handling
Task | CS Lead | VP CS | Eng Lead | CEO
------------------------|---------|-------|----------|----
Receive escalation | R | I | I | -
Diagnose issue | R | C | C | -
Communicate to customer | R | A | - | I (major)
Resolve technical issue | C | - | R | -
Close escalation | R | A | I | -
Post-mortem (P0/P1) | C | A | R | I
```
**Common RACI mistakes:**
- Multiple A roles (breaks accountability)
- R and A always same person (defeats the purpose)
- Too many C roles (everyone's consulted, nothing moves)
- Not distinguishing C from I (different obligations)
### DRI (Directly Responsible Individual)
Apple's framework; used widely in fast-moving tech companies. Simpler than RAPID/RACI for internal use.
**The rule:** Every project, deliverable, and decision has exactly one DRI. The DRI is the person who gets credit when it succeeds and gets called on when it fails. No DRI = no accountability.
**DRI requirements:**
- Listed by name in every project brief
- Has authority to make decisions within scope
- Is responsible for communicating status
- Cannot blame lack of resources — their job is to escalate when blocked
**DRI vs. RACI:** Use DRI for project ownership and RACI for process ownership. They complement each other.
### Decision Log
Every significant decision gets logged. Significant = affects more than one team, costs more than $10K, or is difficult to reverse.
```
DECISION LOG FORMAT:
Date: [YYYY-MM-DD]
Decision: [One sentence summary]
Context: [Why was this decision needed? What was the situation?]
Options considered: [What alternatives were evaluated?]
Decision made: [What was decided?]
Rationale: [Why this option?]
Owner: [Who made the final call?]
Reversible: [Yes / No / Partially]
Review date: [When should this decision be revisited?]
Outcome: [Filled in later — what actually happened?]
```
---
## Reporting Templates
### Weekly CEO/COO Dashboard
```
COMPANY HEALTH — WEEK OF [DATE]
REVENUE
ARR: $[X]M (vs. plan: +/-X%, vs. LW: +/-X%)
New ARR this week: $[X]K
Churned ARR: $[X]K
Pipeline (90-day): $[X]M
PRODUCT
Shipped this week: [Brief list]
P0/P1 incidents: [Count] — [1-line summary if any]
Deploy frequency: [X per week]
CUSTOMER
Active customers: [X]
NPS (rolling 30d): [X]
Open escalations: [X] (P0: [X], P1: [X])
PEOPLE
Headcount: [X] (vs. plan: [X])
Open reqs: [X]
Attrition (30d): [X]
CASH
Cash on hand: $[X]M
Burn (last 30d): $[X]M
Runway: [X] months
🔴 ISSUES (needs leadership attention):
•
•
🟡 WATCH (monitor, no action yet):
•
🟢 WINS:
•
```
### Monthly Investor/Board Update
```
[COMPANY NAME] — MONTHLY UPDATE — [MONTH YEAR]
THE HEADLINE
[2-3 sentences: what was the defining story of this month?]
KEY METRICS
| Metric | [Month] | vs. Prior | vs. Plan |
|--------|---------|-----------|----------|
| ARR | | | |
| MRR Added | | | |
| Churn | | | |
| NRR | | | |
| Burn | | | |
| Runway | | | |
WINS
1. [Specific, concrete win with numbers]
2. [Second win]
3. [Third win]
CHALLENGES
1. [Honest description of challenge + what you're doing about it]
2. [Second challenge]
KEY DECISIONS MADE
• [Decision + brief rationale]
ASKS FROM INVESTORS
• [Specific ask with context — intros, advice, etc.]
NEXT MONTH PRIORITIES
1.
2.
3.
```
### Quarterly OKR Progress Report
```
Q[X] OKR PROGRESS — [COMPANY NAME]
SCORING GUIDE:
🟢 On track (>70% confidence of hitting target)
🟡 At risk (50-70% confidence)
🔴 Off track (<50% confidence)
COMPANY OBJECTIVES:
O1: [Objective title]
KR1.1: [Key Result] ............... [X]% 🟢
KR1.2: [Key Result] ............... [X]% 🟡
Objective confidence: 🟢 | Notes: [1 line]
O2: [Objective title]
KR2.1: [Key Result] ............... [X]% 🔴
KR2.2: [Key Result] ............... [X]% 🟢
Objective confidence: 🟡 | Notes: [1 line]
FUNCTIONAL OBJECTIVES:
[Same format per function]
OVERALL QUARTER HEALTH: 🟡
Summary: [2-3 sentences on overall trajectory]
TOP 3 ACTIONS TO GET BACK ON TRACK:
1. [Action + owner + deadline]
2.
3.
```
---
## Cadence Anti-Patterns to Eliminate
| Anti-Pattern | What It Looks Like | Fix |
|---|---|---|
| **Meeting creep** | Calendar blocks added over time, never removed | Quarterly calendar audit — delete all recurring meetings, re-add only what's essential |
| **Update theater** | Meetings where people read from slides | Require pre-reads; ban in-meeting presentations |
| **Decision avoidance** | Topics recur across multiple meetings | Assign a D (decider) before the meeting. If no D, don't hold the meeting. |
| **Sync for async** | Using meetings for information sharing | Move updates to Loom/Slack; protect sync time for discussion |
| **HIPPO problem** | Highest-paid person in room wins | Structure discussions so data is presented before opinions |
| **Retrospective theater** | Retros with no action items | Every retro must produce ≥1 committed change |
| **Silent agenda** | Agenda not shared until meeting starts | Agendas published 24h in advance, required reading |
---
*Cadence framework synthesized from Amazon's PR/FAQ culture, Google's OKR playbook, GitLab's remote work handbook, and operational patterns from 50+ Series A–C companies.*
FILE:coo-advisor/references/process_frameworks.md
# Process Frameworks for Startup Operations
> Theory of Constraints, Lean, process mapping, automation, and change management — applied to real startup contexts, not factory floors.
---
## Part 1: Theory of Constraints (TOC) Applied to Startups
### What TOC Actually Says
Eliyahu Goldratt's core insight: **every system has exactly one constraint that limits throughput.** Improving anything other than the constraint is waste. The goal isn't to optimize every function — it's to identify the single bottleneck and exploit it until a new constraint emerges.
**The Five Focusing Steps:**
1. **Identify** the constraint — what limits the system's output?
2. **Exploit** it — get maximum output from the constraint without adding resources
3. **Subordinate** everything else — other activities serve the constraint's needs
4. **Elevate** it — add resources to increase constraint capacity
5. **Repeat** — when the constraint moves, find the new one
### Finding the Constraint in Your Startup
The constraint is almost never where people think it is. Sales thinks it's Marketing. Engineering thinks it's Product. Everyone thinks it's someone else.
**Method:** Map your value stream (see Part 3), measure throughput at each step, find the step with the lowest throughput or the highest queue in front of it.
**Common startup constraints by stage:**
| Stage | Most Common Constraint | Why |
|-------|----------------------|-----|
| Pre-PMF | Learning speed | Not enough customer feedback cycles |
| Series A | Sales capacity | Demand > sales team's ability to close |
| Series B | Engineering velocity | Product backlog growing faster than shipping rate |
| Series C | Onboarding throughput | New customer volume > CS team's onboarding capacity |
| Growth | Hiring throughput | Headcount plan > recruiting team's capacity |
### Applying TOC to Product Development
**The five visible constraints in product development:**
**1. Requirements clarity**
*Symptom:* Engineering asks for clarification mid-sprint. Tickets re-opened. Scope creep.
*Fix:* Never pull a story into sprint until acceptance criteria are written and reviewed. Product manager must be available same-day for clarification.
**2. Review and approval bottleneck**
*Symptom:* PRs sit unreviewed for >24 hours. Deploys waiting for sign-off.
*Fix:* Code review SLA: 2-hour response for small PRs (<100 lines), 4-hour for medium. Design reviews: 24-hour turnaround. Anyone waiting >SLA can escalate to manager.
**3. QA throughput**
*Symptom:* "Done" pile grows faster than QA can test. Release day crunch.
*Fix:* QA is pulled into sprint planning and sprint review. Testing starts as features finish, not all at end. Automated test coverage as a sprint exit criterion.
**4. Deployment pipeline speed**
*Symptom:* Deploy takes 45+ minutes. Engineers wait. Hotfix urgency causes dangerous shortcuts.
*Fix:* Measure deploy time weekly. Set target (10 min for most apps). Build optimization into engineering roadmap as a real ticket.
**5. Feedback loop latency**
*Symptom:* You ship features and don't know if they worked for weeks.
*Fix:* Every shipped feature has instrumented metrics reviewed within 5 business days. If no metrics exist, feature doesn't ship.
### Applying TOC to Sales
**The sales pipeline as a system of constraints:**
```
Lead generation → Qualification → Demo → Proposal → Negotiation → Close
[X] → [X] → [X] → [X] → [X] → [X]
Measure: conversion rate and time-in-stage at each step.
The constraint is the step with the LOWEST conversion rate × volume.
```
**Example diagnosis:**
- Lead → Qualified: 40% conversion, 2 days
- Qualified → Demo: 80% conversion, 5 days ← High conversion but slow (queue)
- Demo → Proposal: 60% conversion, 3 days
- Proposal → Close: 30% conversion, 14 days ← **Constraint** (lowest conversion)
*Diagnosis:* Proposals are being sent to wrong buyers or proposals aren't compelling. Fix: proposal template audit, champion coaching, economic buyer access earlier in process.
---
## Part 2: Lean Operations for Tech Companies
### The Lean Toolkit (What's Actually Useful)
Lean Manufacturing was designed for car factories. Most of the original toolkit doesn't apply to software. Here's what does:
**Value Stream Mapping** — Map the full flow of work from customer request to delivery. Label value-add time vs. wait time. Most processes are 90% wait time and 10% actual work.
**5S** — Sort, Set in order, Shine, Standardize, Sustain. Applied to digital work:
- *Sort:* Delete unused tools, channels, documents
- *Set in order:* Organize information architecture so things are findable
- *Shine:* Regular cleanup sprints (documentation, tech debt, tool hygiene)
- *Standardize:* Templates, conventions, naming standards
- *Sustain:* Assign owners; entropy is the default state
**Pull vs. Push** — Don't push work onto people's plates. Pull = people take work when they have capacity. Push = work is assigned to people regardless of capacity. Most companies push; lean companies pull.
**Kaizen** — Continuous small improvements. Build this into your operating rhythm:
- Weekly: each team identifies one small improvement to their process
- Monthly: review and close out improvement items
- Quarterly: broader process retrospective
**Waste Categories (TIMWOODS) — Applied to Operations:**
| Waste Type | Factory Example | Startup Example |
|-----------|----------------|-----------------|
| **T**ransportation | Moving parts | Handing off work between tools with no integration |
| **I**nventory | Parts stockpile | Unreviewed PRs, unworked backlog items, unread reports |
| **M**otion | Worker movement | Context switching between apps / communication channels |
| **W**aiting | Machine idle | Waiting for approvals, waiting for data, waiting for decisions |
| **O**verproduction | Making more than needed | Features built that weren't validated |
| **O**verprocessing | Extra steps | 6-step approval for $200 purchase |
| **D**efects | Rework | Bug fixes, incorrect specs, miscommunicated requirements |
| **S**kills | Underutilized talent | Senior engineers doing manual QA |
**Exercise:** For your most important process, walk through each waste category and estimate hours/week wasted. This exercise typically reveals 20–40% improvement opportunities in the first pass.
### Cycle Time and Lead Time
**Lead time:** Time from when a request enters the system to when it exits (customer perspective).
**Cycle time:** Time a unit of work is actively being worked on (team perspective).
```
Lead Time = Cycle Time + Wait Time
```
Most teams only measure cycle time. Customers only experience lead time. The gap between the two is pure waste.
**Measuring in your context:**
- Engineering: Lead time = ticket created → in production. Cycle time = in progress → PR merged.
- Sales: Lead time = lead created → closed won. Cycle time = demo completed → proposal sent.
- CS: Lead time = ticket opened → customer confirms resolved. Cycle time = ticket in-progress → resolution sent.
**Improvement pattern:**
1. Measure lead time (not just cycle time)
2. Find the steps where tickets sit waiting
3. Remove the wait (automation, reduced approval layers, clearer handoff criteria)
### WIP Limits
Work-In-Progress limits prevent the multi-tasking trap. When people work on 5 things simultaneously, each thing takes 5x longer and quality drops.
**Recommended WIP limits:**
- Individual IC: 2–3 active items at once
- Team sprint: WIP = number of engineers × 1.5
- Leadership team: No more than 3 company-level priorities per quarter
**Implementation:** In Jira/Linear, add a WIP column. Set a hard limit. When the column is full, no new work starts until something ships.
---
## Part 3: Process Mapping Techniques
### When to Map a Process
Map a process when:
- It's done by more than 2 people
- It fails regularly (errors, rework, complaints)
- It needs to scale (you're about to add people or volume)
- You're automating it (you must understand the manual process first)
- You're onboarding someone new to it
Don't map processes that are genuinely ad-hoc, one-person, or will change significantly in the next 90 days.
### The Three Levels of Process Maps
**Level 1: Swim Lane Map (for cross-functional processes)**
Best for: Customer onboarding, sales-to-CS handoff, escalation handling, hiring
```
Example: Sales to CS Handoff
| Sales AE | Sales Ops | CS Manager | CS Rep |
--------|---------------|---------------|---------------|---------------|
Step 1 | Close deal | | | |
Step 2 | Fill handoff | | | |
| doc | | | |
Step 3 | | Route to CS | | |
Step 4 | | | Review & | |
| | | assign | |
Step 5 | | | | Send welcome |
Step 6 | | | | Schedule kick-|
| | | | off |
```
**Level 2: Flowchart (for decision-heavy processes)**
Best for: Escalation routing, incident response, approval workflows
Use standard symbols:
- Rectangle = action/task
- Diamond = decision (yes/no branch)
- Oval = start/end
- Parallelogram = input/output
**Level 3: Work Instructions (for execution-level processes)**
Best for: Checklists, SOPs, how-to guides
Format:
```
Process: [Name]
Owner: [Role]
Last reviewed: [Date]
Trigger: [What starts this process]
Step 1: [Action] — [Who does it] — [Tool used] — [Expected output]
Step 2: ...
Exceptions:
- If [condition], then [alternative action]
Done when: [Definition of done]
```
### Process Audit Technique
Run this quarterly on your most critical processes:
**1. Walk the process** — Literally follow a unit of work from start to finish. Ask the people doing it, not the people managing it.
**2. Measure three numbers:**
- How long does it actually take? (lead time)
- How often does it go wrong? (error/rework rate)
- What's the cost of a failure? (downstream impact)
**3. Score it:**
```
PROCESS HEALTH SCORE:
Lead time vs. target: [+2 on target / 0 delayed / -2 significantly delayed]
Error rate: [+2 <5% / 0 5-15% / -2 >15%]
Documented: [+1 yes / -1 no]
Owner named: [+1 yes / -1 no]
Last reviewed (< 6 months): [+1 yes / -1 no]
Max: 7. Score <3 = needs immediate attention.
```
---
## Part 4: Automation Decision Framework
### The "Should I Automate This?" Test
Not everything should be automated. Bad automation of a broken process = faster broken process.
**The five-question filter:**
1. **Is the process stable?** If it changes monthly, automate later. Automating unstable processes locks in the wrong behavior.
2. **How often does it happen?** Weekly or more frequent = good candidate. Monthly or less = probably not worth it.
3. **What's the error rate without automation?** If the manual process is accurate 95%+ of the time, automation ROI is lower.
4. **What's the cost of failure?** Customer-facing, compliance, or financial processes deserve higher automation priority than internal reporting.
5. **Is the process well-documented?** If you can't describe it in a flowchart, you can't automate it. Document first.
### Automation ROI Calculation
```
Annual hours saved = (minutes per occurrence / 60) × occurrences per year
Annual labor cost saved = hours saved × fully-loaded cost per hour
Net annual value = labor cost saved + error reduction value + speed improvement value
Build/buy cost = development time + maintenance overhead
Payback period = build/buy cost ÷ net annual value
Rule of thumb: automate if payback period < 12 months
```
**Example:**
- Process: Weekly sales report compilation
- Time: 3 hours/week manually
- Fully-loaded cost: $75/hour
- Annual manual cost: 3 × 52 × $75 = $11,700
- Automation cost: 40 hours to build = $3,000
- Payback: 3,000 ÷ 11,700 = 3 months → **Automate**
### Automation Tiers
**Tier 1: No-code automation** (0–8 hours to implement)
- Tools: Zapier, Make (Integromat), n8n, HubSpot workflows
- Use for: Notification triggers, data syncs between tools, simple conditional routing
- Example: New customer in CRM → create CS ticket → send welcome Slack message
**Tier 2: Low-code automation** (8–40 hours to implement)
- Tools: Retool, internal scripts, Google Apps Script, Airtable Automations
- Use for: Internal dashboards, data transformation, approval workflows
- Example: Weekly metrics compilation from Salesforce + Mixpanel + HubSpot into Notion dashboard
**Tier 3: Engineered automation** (40+ hours to implement)
- Built by engineering team as product/infrastructure work
- Use for: Customer-facing workflows, compliance-critical processes, high-volume operations
- Example: Automated customer health score calculation → CS alert → playbook trigger
### Automation Prioritization Matrix
```
HIGH FREQUENCY
|
Tier 1 now | Tier 2-3 now
(quick win) | (high-value)
|
LOW VALUE ________________|________________ HIGH VALUE
|
Don't bother | Plan for later
| (when it's bigger)
|
LOW FREQUENCY
```
Place each manual process in the quadrant. Execute top-right first, Tier 1 items second.
### Automation Governance
As automation grows, it needs governance:
**Automation registry:** Maintain a list of all automations with:
- Name and description
- Owner (person responsible if it breaks)
- Tools used
- Trigger and action
- Last tested date
- Business impact if down
**Review cadence:** Quarterly review of automation registry. Kill automations nobody uses.
**Failure alerting:** Every production automation must have failure notifications sent to a named owner. Silent failures are worse than no automation.
---
## Part 5: Change Management for Process Rollouts
### Why Process Changes Fail
Most process changes fail not because the process is wrong, but because of how it's rolled out. Common failure modes:
- **Top-down dictate:** Process designed by leadership, announced to team, implemented poorly because people weren't involved and don't understand why.
- **No training:** "Here's the new process" with no demonstration or practice.
- **No feedback loop:** Process is rolled out and never adjusted based on what the team discovers.
- **No accountability:** Process is optional in practice because there are no consequences for ignoring it.
- **Old behavior still possible:** You introduce a new tool but don't turn off the old way.
### The Change Management Framework (ADKAR)
ADKAR (Awareness, Desire, Knowledge, Ability, Reinforcement) is the most practical model for operational change.
**A — Awareness:** Does everyone understand WHY the change is needed?
- Don't just announce the new process — explain what was broken about the old one
- Share the data: "Our current onboarding takes 45 days, customers who onboard faster have 2x better retention. The new process targets 21 days."
**D — Desire:** Do people want to change?
- Resistance is information. Listen to it.
- Involve front-line workers in process design. People support what they help build.
- Address WIIFM (What's In It For Me) for each affected group
**K — Knowledge:** Do people know HOW to do the new process?
- Write it down (work instructions format above)
- Run live demos and practice sessions
- Create a "first time" checklist
**A — Ability:** Can people actually do the new process?
- Identify where people get stuck (first 2 weeks of rollout)
- Have a designated expert for questions
- Remove friction: if the new process requires 3 clicks where the old required 1, people will revert
**R — Reinforcement:** Does the change stick?
- Measure adoption (are people actually using the new process?)
- Celebrate early adopters
- Address non-adoption promptly — call it out without shame
### Change Rollout Checklist
```
PRE-LAUNCH:
□ Process designed and documented
□ Stakeholders identified (people affected by change)
□ Champions identified (people who will help adoption)
□ Training materials created
□ Success metrics defined (how will you know it worked?)
□ Rollback plan documented (what if it breaks something?)
□ Launch timeline set and communicated
LAUNCH WEEK:
□ Announcement sent with WHY, WHAT, and WHEN
□ Training sessions held (at least 2 options for different schedules)
□ Feedback channel opened (Slack thread, form, or dedicated meeting)
□ Champions briefed to support peers
2-WEEK CHECK:
□ Adoption rate measured
□ Friction points documented
□ Quick fixes implemented
□ Feedback reviewed and responded to
30-DAY REVIEW:
□ Success metrics reviewed vs. baseline
□ Process adjustments made based on learnings
□ Champions recognized
□ Process documentation updated with lessons learned
90-DAY CLOSE:
□ Full adoption confirmed or non-adoption addressed
□ Process owners confirmed
□ Handoff to BAU (business as usual) operations
```
### Managing Resistance
**Types of resistance and responses:**
| Resistance Type | What It Sounds Like | Right Response |
|----------------|---------------------|----------------|
| Legitimate concern | "This process won't work because X happens" | Acknowledge, investigate, fix or explain |
| Anxiety | "I don't know how to do this" | Training, support, reassurance |
| Loss of control | "This takes away my judgment" | Involve them in design; give them ownership of part of it |
| Passive non-compliance | Silent ignoring of the new process | Direct conversation; make it visible and required |
| Organizational inertia | "We've always done it this way" | Show the cost of the status quo in concrete terms |
**The three levers of adoption:**
1. **Make the new way easier than the old way** (remove the old path if possible)
2. **Make non-adoption visible** (dashboards showing who's using the process)
3. **Connect process to meaningful outcomes** (show how it affects things people care about)
### Process Documentation Standards
Every process should have exactly one owner responsible for keeping it current.
**Minimum documentation for any process:**
- **Process name** and one-sentence purpose
- **Owner:** Named individual, not a team
- **Trigger:** What starts this process
- **Steps:** Written at the level that a new employee could execute
- **Exceptions:** Common edge cases and how to handle them
- **Done definition:** How you know the process is complete
- **Review date:** Set a future date when this gets reviewed
**Documentation debt kills scale.** The most valuable time to document is right after you've run the process for the third time — you've found the edge cases, you know the real steps, and the process is still fresh.
---
## Framework Selection Guide
| Situation | Framework |
|-----------|-----------|
| We're slow and can't figure out why | Theory of Constraints — find the bottleneck |
| We have lots of waste and overhead | Lean — waste audit (TIMWOODS) |
| Process is inconsistent across team | Process mapping — Level 1 swim lane |
| Deciding what to automate | Automation decision framework + ROI calc |
| New process keeps getting ignored | ADKAR change management |
| Unclear who's responsible | RACI or DRI framework |
| Too many decisions escalating to leadership | RAPID decision rights |
---
*Frameworks synthesized from: Eliyahu Goldratt's The Goal and Critical Chain; Womack and Jones' Lean Thinking; Prosci ADKAR model; Scaled Agile Framework (SAFe) process guidance; operational playbooks from Stripe, Airbnb, and Shopify operations teams.*
FILE:coo-advisor/references/scaling_playbook.md
# Scaling Playbook: What Breaks at Each Growth Stage
> Compiled from patterns across 100+ high-growth companies. Not theory — this is what actually breaks and what to do about it.
---
## How to Use This Playbook
Each stage section covers:
1. **What breaks** — the specific failure modes that kill companies at this stage
2. **Hiring** — who to bring in and when
3. **Process** — what to formalize vs. keep loose
4. **Tools** — infrastructure that unlocks the next stage
5. **Communication** — how information flow changes
6. **Culture** — what to protect and what to let go
**Benchmarks are medians** — your mileage varies by sector, geography, and business model.
---
## Stage 0: Pre-Seed / Seed ($0–$2M ARR, 1–15 people)
### Key Benchmarks
| Metric | Benchmark |
|--------|-----------|
| Revenue per employee | $0–$100K (still finding PMF) |
| Manager:IC ratio | N/A (no managers) |
| Burn multiple | 2–5x (acceptable) |
| Runway | 12–18 months minimum |
| Time-to-hire | 2–4 weeks |
### What Breaks
**Premature process.** The #1 mistake at seed stage is adding process before you have a repeatable model. Sprint ceremonies, OKR frameworks, and performance reviews are all theater when you haven't found PMF. Every hour spent in process is an hour not spent learning.
**Wrong first hires.** Hiring "senior" people who've only worked in structured environments. You need people who can operate in chaos, not people who expect process to already exist.
**Founder communication bottleneck.** Founders try to be in every decision. Fine at 5 people, fatal at 12. No written decisions means knowledge lives in founders' heads — unscalable.
**Technical debt accepted as strategy.** "We'll fix it later" said about core data models, auth systems, or billing. Later comes at Series A and it costs 3x more to fix.
### Hiring
- **Don't hire for scale you don't have.** Hire for the next 12 months.
- **First 10 hires set culture permanently.** Get them wrong and you'll spend years correcting.
- **Hire athletes, not specialists.** Generalists who can do multiple jobs outperform specialists at this stage.
- **Avoid VP titles early.** Inflated titles block future hires and create expectations you can't meet.
- **Founder-referral bias is real.** Your network is homogeneous. Force diversity early.
**Who to hire first (in rough order):**
1. Engineers who can ship product (2–3 generalists)
2. First sales/GTM if B2B (founder-led sales first, then one closer)
3. Designer/product (often a hybrid)
4. Customer success (often a founder at first)
### Process
**Formalize nothing before PMF.** Literally. Run on Slack, shared docs, and founder judgment.
**After PMF signals appear, formalize only:**
- How you handle customer escalations
- How you deploy code (even basic CI/CD)
- How you onboard new hires (a 1-page checklist is enough)
**Decision rule:** If a founder has to answer the same question three times, write it down. Once.
### Tools
| Function | Seed-Stage Tool |
|----------|----------------|
| Communication | Slack + Google Workspace |
| Project tracking | Linear or Notion (pick one, stay consistent) |
| CRM | HubSpot free or Notion |
| Engineering | GitHub + basic CI (GitHub Actions) |
| Finance | Brex/Mercury + QuickBooks |
| HR | Rippling or Gusto (basic) |
| Analytics | Mixpanel or PostHog (free tier) |
**Rule:** One tool per function. No tool sprawl. Every extra tool is a coordination tax.
### Communication
- **Weekly all-hands** (30 min max). What shipped, what's stuck, what's next.
- **No status meetings.** Anyone can see status in Linear/Notion.
- **Founder write-ups.** Every major decision gets a 1-paragraph Slack post explaining *why*.
- **Group chat discipline.** One channel per project/customer. Inbox zero mentality.
### Culture
**What to build deliberately:**
- High ownership: everyone acts like they own the company, because they do
- Direct feedback: brutal honesty delivered with care
- Bias to ship: done > perfect
- Customer obsession: founders talk to customers weekly
**What to watch for:**
- "Hero culture" where one person saves everything — unsustainable
- Over-indexing on culture fit (code for homogeneity)
- Avoidance of conflict — mistaking silence for agreement
---
## Stage 1: Series A ($2–$10M ARR, 15–50 people)
### Key Benchmarks
| Metric | Benchmark |
|--------|-----------|
| Revenue per employee | $100–$200K |
| Manager:IC ratio | 1:6–1:8 |
| Burn multiple | 1.5–2.5x |
| Sales efficiency (CAC payback) | <18 months |
| Churn (B2B SaaS) | <10% net annual |
| Engineering velocity | Feature shipped every 1–2 weeks |
| Time-to-hire | 4–6 weeks |
| Offer acceptance rate | >80% |
### What Breaks
**Founder-as-manager bottleneck.** At 20+ people, founders can't manage everyone. The first layer of management needs to appear — and it's usually picked wrong (best IC ≠ best manager).
**Tribal knowledge explosion.** "Ask Sarah" stops working when Sarah has 15 things open. Documentation becomes critical — not for bureaucracy, but because institutional knowledge is now a flight risk.
**Sales process fragmentation.** Without a defined sales process, every rep closes differently. You can't train, debug, or scale what you can't see.
**Scope creep in product.** With Series A money comes investor pressure to expand scope. Teams try to build three things at once and ship nothing well.
**Compensation chaos.** Early employees got equity-heavy deals. New hires get market cash. Someone compares, someone gets upset. No comp philosophy = constant re-negotiation.
**Recruiting becomes a job in itself.** Founders can't hire 30 people themselves. First dedicated recruiter needed by 25 people.
### Hiring
**Who to hire at Series A:**
- **Head of Engineering** (if founder is CTO): needs to be an operator, not just an architect
- **First Sales Manager** (when you have 3+ reps): don't promote the best seller
- **HR/People Ops** (generalist, by 30 people): comp, compliance, recruiting coordination
- **Finance** (fractional CFO or strong controller): Series A board needs real numbers
- **Customer Success Lead**: retention is everything at this stage
**Hiring mistakes to avoid:**
- Hiring "big company" execs who need large teams and established process
- Assuming your Series A lead can recruit (they can intro, not close)
- Taking too long — top candidates have 2–3 offers. Move in <2 weeks from first call to offer.
**Leveling:** Build a simple career ladder *before* the compensation complaints start. 3–4 levels per function is enough.
### Process
**What to formalize at Series A:**
1. **Sprint planning** (2-week sprints, public roadmap)
2. **Sales process** (defined stages with entry/exit criteria)
3. **Onboarding** (30/60/90 day plan for each function)
4. **1:1 cadence** (weekly for direct reports, bi-weekly for skip-levels)
5. **Incident response** (P0/P1/P2 definition, on-call rotation)
6. **Quarterly planning** (OKRs or goals framework — keep it lightweight)
**What to keep loose:**
- Internal project process (let teams self-organize)
- Meeting formats (let teams evolve their own rituals)
- Tool selection within approved stack
**Documentation standard:** Write decisions down in a shared wiki. "Decision log" with date, decision, context, owner, and outcome. Takes 5 minutes, saves hours.
### Tools
| Function | Series A Tool |
|----------|--------------|
| Project/Product | Linear + Notion |
| CRM | HubSpot or Salesforce (Starter) |
| Engineering | GitHub + CI/CD pipeline + Sentry |
| HR/People | Rippling or Lattice (performance) |
| Finance | NetSuite or QBO + Brex |
| Analytics | Mixpanel/Amplitude + Looker (or Metabase) |
| Customer Success | Intercom + HubSpot or Zendesk |
| Docs | Notion or Confluence |
### Communication
**Introduce structured communication layers:**
1. **Company all-hands** (monthly, 60 min): CEO share, metrics review, team spotlights, Q&A
2. **Leadership sync** (weekly, 60 min): cross-functional issues, blockers, priorities
3. **Team standups** (async or 15 min daily): what's in progress, what's blocked
4. **1:1s** (weekly): direct report health, career, performance
5. **Written updates** (weekly to investors + board): CEO memo format
**Information hierarchy:** Everyone in the company should know: (1) company goals this quarter, (2) their team's goals, (3) what they personally own. If they don't, your communication structure is broken.
### Culture
**Deliberate culture work starts here.** You're too big for culture to be accidental.
- **Write down values.** Real values with examples of what they look like in action. Not "integrity" — "we tell investors bad news before we tell them good news."
- **Performance management.** First PIPs (Performance Improvement Plans) happen at this stage. Handle them well — the team is watching.
- **Equity culture.** Make sure people understand what their equity is worth in different outcomes. Lack of transparency breeds resentment.
- **First layoff plan.** Even if you never use it, know the criteria. Reactive layoffs destroy trust; plan-based ones (even painful) preserve it.
---
## Stage 2: Series B ($10–$30M ARR, 50–150 people)
### Key Benchmarks
| Metric | Benchmark |
|--------|-----------|
| Revenue per employee | $150–$300K |
| Manager:IC ratio | 1:5–1:7 |
| Burn multiple | 1.0–1.5x |
| CAC payback | <12 months |
| NRR (net revenue retention) | >110% |
| Engineering: Product ratio | ~3:1 |
| Sales: CS ratio | ~3:1 |
| Time-to-hire (senior) | 6–10 weeks |
| Annual attrition | <15% voluntary |
### What Breaks
**Middle management void.** You now have managers managing managers. The "player-coach" model breaks — people can't be ICs and managers simultaneously at this scale. Force the choice.
**Planning misalignment.** Sales promises what product hasn't built. Product builds what customers didn't ask for. Engineering ships what QA didn't test. Fixing this requires cross-functional planning ceremonies.
**Data fragmentation.** Five different versions of "how are we doing." Sales sees Salesforce. Product sees Amplitude. Finance sees spreadsheets. Nobody agrees. You need a single source of truth.
**Process debt.** The Series A processes are starting to creak. Onboarding that worked for 5 hires/quarter doesn't work for 20. Customer escalation paths built for 50 customers fail at 500.
**Cultural fragmentation.** Engineering culture ≠ Sales culture ≠ Support culture. Sub-cultures form. The shared identity you had at 30 people requires active work to maintain at 100.
**The "brilliant jerk" problem.** High performers with bad behavior were tolerated early. Now they're managers with bad behavior, and it's systemic. Act decisively or lose your best people.
### Hiring
**Who to hire at Series B:**
- **COO or VP Operations**: founder is overwhelmed, someone needs to run the machine
- **VP Sales**: first Sales Manager won't scale to 20-rep org
- **VP Marketing**: demand gen and brand need dedicated ownership
- **Dedicated Recruiting**: 2–3 recruiters minimum; you're hiring 30–50 people/year
- **Data/Analytics**: dedicated analyst or data engineer to consolidate reporting
- **Legal counsel**: fractional or in-house; contracts and compliance are getting complex
**The "big company exec" trap.** Series B is when companies hire their first VP from FAANG or a large SaaS company. 60% of these fail within 18 months. They're used to: large teams, established brand, existing process, political navigation. They struggle with: scrappy execution, no support staff, ambiguous direction. Vet explicitly for startup experience.
**Span of control.** At this stage, hold managers to 5–8 direct reports. More than 8 = no time for actual management. Less than 3 = management overhead isn't justified.
### Process
**What to formalize at Series B:**
1. **Quarterly Business Reviews (QBRs)** — every function presents metrics, wins, gaps
2. **Annual planning** — budget, headcount plan, strategic priorities
3. **Cross-functional roadmap alignment** — product/sales/marketing in sync quarterly
4. **Promotion criteria** — written, public, applied consistently
5. **Interview scorecards** — structured interviews with defined rubrics
6. **Change management** — how major process changes get communicated and adopted
7. **Vendor management** — evaluation criteria, approval process, contract management
**SOPs for critical processes:**
- Customer onboarding (if >50 customers)
- Sales handoff from SDR to AE to CS
- Engineering release process
- Incident response playbook
- Contractor/vendor procurement
### Tools
| Function | Series B Tool |
|----------|--------------|
| Project/Product | Jira or Linear (with roadmapping) |
| CRM | Salesforce (full) |
| ERP/Finance | NetSuite |
| HR | Workday or BambooHR + Lattice |
| Analytics | Looker or Tableau + data warehouse |
| Customer Success | Gainsight or ChurnZero |
| Engineering | GitHub Enterprise + full CI/CD + observability |
| Security | 1Password Teams + SSO (Okta) + endpoint management |
### Communication
**At 50+ people, informal communication breaks down.** Information no longer flows naturally — it has to be architected.
**Communication stack:**
- **Monthly all-hands** (90 min): metrics deep-dive, strategy update, team Q&A
- **Weekly leadership team** (90 min): cross-functional priorities, decisions, escalations
- **Bi-weekly skip-levels** (30 min): every manager holds these with their manager's reports
- **Quarterly town halls** (2 hrs): broader context, financial update, roadmap preview
- **Written company update** (bi-weekly): CEO to all-hands via Slack/email
**The information gradient problem.** People at the top know too much. People at the bottom know too little. Fix this with a deliberate "broadcast" culture — any decision affecting more than 5 people gets written up and shared.
### Culture
**Retention becomes an existential issue.** At Series B, you have 50–150 people who've been with you through something hard. They're valuable. And they have options.
- **Career ladders** are non-negotiable by this stage. People leave when they can't see a future.
- **Manager quality** determines retention. Invest in manager training. Run manager effectiveness surveys.
- **Compensation benchmarking** quarterly. If you're more than 10% below market, you're losing people silently.
- **Culture carriers.** Identify the 10–15 people who embody your culture and make them formally responsible for transmitting it. Give them a platform.
---
## Stage 3: Series C ($30–$75M ARR, 150–500 people)
### Key Benchmarks
| Metric | Benchmark |
|--------|-----------|
| Revenue per employee | $200–$400K |
| Manager:IC ratio | 1:5–1:6 |
| Burn multiple | 0.75–1.25x |
| NRR | >115% |
| CAC payback | <9 months |
| Sales cycle (Enterprise) | 60–120 days |
| Engineering team % | 30–40% of headcount |
| Annual attrition target | <12% voluntary |
| Time-to-hire (senior) | 8–12 weeks |
### What Breaks
**Strategy execution gap.** Leadership agrees on strategy. Middle management interprets it differently. ICs execute on their interpretation. By the time work ships, it barely resembles the original strategy. Fix: strategy must cascade in writing with explicit outcomes.
**Process bureaucracy.** The processes you built at Series B start generating bureaucracy. Approval chains lengthen. Simple decisions require three meetings. The antidote is explicit process owners empowered to eliminate friction.
**Org design complexity.** Do you have functional teams (all engineers in one org) or product teams (engineers embedded in product squads)? The answer affects everything: career paths, knowledge sharing, delivery speed. Most companies get this wrong twice before getting it right.
**Geographic complexity.** First international office or remote-heavy team introduces timezone, communication, and culture challenges that don't exist when everyone is in one room.
**Leadership team dysfunction.** Seven VPs who were all individual contributors two years ago are now running $10M+ organizations. Some have grown into it. Some haven't. This is the stage where hard leadership team changes happen.
### Hiring
**Series C hiring is about depth, not breadth.** You have functional coverage — now hire people who go deep within functions.
- **Functional leaders' deputies**: VP Engineering needs a Director of Platform Engineering, Director of Product Engineering, etc.
- **Internal promotions**: 40–60% of leadership roles should be filled internally by now. If you're hiring externally for everything, you've failed at development.
- **Specialists**: Security, data science, UX research, RevOps — functions that were "shared" become dedicated.
- **General Counsel**: Legal volume justifies full-time counsel.
**Headcount planning discipline.** Every hire should have a business case. "The team is busy" is not a business case. "This role will unlock $X in revenue or save Y hours/week" is a business case.
### Process
**Process consolidation.** Audit every process. Kill anything that doesn't have a clear owner and clear outcome. The average Series C company has 40% more process than it needs.
**Key processes to have locked at Series C:**
1. **Annual planning cycle** (strategy → goals → headcount → budget)
2. **Quarterly operating review** (progress against plan, forecast, adjustments)
3. **Product development lifecycle** (discovery → design → build → launch → measure)
4. **Revenue operations** (forecasting, pipeline management, territory planning)
5. **People operations** (performance cycles, promotion cadence, compensation philosophy)
6. **Risk management** (operational, security, compliance, legal)
**Delegation architecture.** At 200+ people, the COO cannot know about every decision. Build explicit decision rights: what decisions require CEO/COO approval vs. VP vs. Director vs. IC.
### Tools
**Consolidate the tech stack.** By Series C, you have tool sprawl. The average 200-person company has 100+ SaaS tools. 40% are redundant. Consolidation saves $200–500K/year and reduces security surface.
**Must-have by Series C:**
- Enterprise SSO (Okta/Google Workspace with MFA everywhere)
- Data warehouse (Snowflake/BigQuery) + BI layer
- HRIS with performance management (Workday, Rippling, BambooHR)
- Revenue intelligence (Gong, Chorus)
- Security tooling (endpoint, SIEM basics, SOC 2 compliance)
### Communication
**Internal comms becomes a function.** You cannot rely on ad-hoc Slack and email at 200+ people. Someone needs to own internal communications.
- **Monthly CEO update** (written, 500 words max): company performance, strategic context, what's next
- **Quarterly all-hands** (2 hrs): comprehensive business review, open Q&A
- **Leadership alignment sessions** (quarterly): leadership team off-site to calibrate on strategy
- **Manager cascade** (after every major announcement): managers brief their teams with tailored context
### Culture
**Culture is now a function, not an instinct.** By Series C, your original culture-carriers are managers or have left. New people joining have never seen how you worked when you were small.
- **Culture explicitly documented** — not a values poster, a behavioral handbook
- **Onboarding redesigned** for culture transmission at scale
- **Manager enablement** — managers are your primary culture delivery mechanism; invest heavily
- **Listening infrastructure** — eNPS quarterly, exit interviews, skip-level feedback — all analyzed systematically
---
## Stage 4: Growth Stage ($75M+ ARR, 500+ people)
### Key Benchmarks
| Metric | Benchmark |
|--------|-----------|
| Revenue per employee | $300–$600K |
| Manager:IC ratio | 1:4–1:6 |
| Burn multiple (path to profitability) | <0.5x |
| NRR | >120% |
| S&M as % of revenue | 25–35% |
| R&D as % of revenue | 15–25% |
| G&A as % of revenue | 8–12% |
| Rule of 40 | >40 (growth rate + profit margin) |
| Annual attrition target | <10% voluntary |
### What Breaks
**Execution at scale.** The larger you are, the harder it is to move fast. The average decision at a 500-person company takes 3x longer than at a 50-person company. This is not inevitable — but fixing it requires explicit investment.
**Internal politics.** Org boundaries create fiefdoms. VPs protect headcount. Teams optimize for their metrics at the expense of company metrics. This is the #1 culture problem at scale.
**Innovation starvation.** The core business is optimized, but new bets are starved of resources. The people working on new initiatives are constrained by processes designed for a mature product. Structural solution required: separate P&L, separate team, different metrics.
**Middle management bloat.** Growth-stage companies often have too many managers and not enough ICs. A manager managing one other manager managing three ICs is a 3-level chain where 2 people add no value. Flatten aggressively.
### Hiring
**You're now competing for talent with FAANG.** Your advantage is mission, equity, and the ability to have impact. Candidates who want to join a Fortune 500 will not join you. Stop trying to attract them.
- **Leadership pipeline**: promote from within at 50%+ for senior roles
- **Talent density over headcount**: 30 strong engineers > 50 average engineers
- **Diverse hiring**: by this stage, lack of diversity is a business problem, not just an ethical one
### Operational Priorities at Scale
1. **Operational efficiency over growth**: headcount growth should lag revenue growth
2. **Process ownership**: every major process has a named owner accountable for outcomes
3. **Quarterly operating model**: budget vs. actual, full P&L transparency to VP level
4. **Automation**: manual operational processes that cost >40 hrs/week should be automated
---
## Cross-Stage Principles
### The Three Things That Kill Companies at Every Stage
1. **Running out of cash before finding the next unlock** — runway management is sacred
2. **Hiring the wrong person for a critical role** — one bad VP can set you back 18 months
3. **Moving too slowly** — market timing matters; perfect is the enemy of shipped
### The Org Design Progression
```
Seed: Flat | Everyone reports to founder | No structure
Series A: Functional pods | First-line managers | Light structure
Series B: Functional departments | VPs emerge | Defined structure
Series C: Business units or product squads | Directors + VPs | Full structure
Growth: Divisional or matrix | EVPs/SVPs | Corporate structure
```
### Revenue per Employee by Function (B2B SaaS benchmarks)
| Function | Series A | Series B | Series C | Growth |
|----------|----------|----------|----------|--------|
| Engineering | $400K | $500K | $600K | $700K |
| Sales | $250K | $350K | $450K | $500K |
| Customer Success | $300K | $400K | $500K | $600K |
| Marketing | $500K | $700K | $900K | $1M+ |
| G&A | $600K | $800K | $1M | $1.2M |
*Revenue per employee = ARR / headcount in function*
### The Management Span Rule
- **Individual contributors being managed**: 1 manager per 6–8 ICs
- **Managers being managed**: 1 director per 4–6 managers
- **Directors being managed**: 1 VP per 3–5 directors
- **VPs being managed**: 1 C-level per 5–8 VPs
Violation of this creates either manager burnout (too wide) or management theater (too narrow).
---
## Red Flags by Stage
| Stage | Red Flag | Likely Cause |
|-------|----------|-------------|
| Seed | Missed 3+ product deadlines | Wrong team or unclear prioritization |
| Series A | Churn >20% | PMF not actually found, or CS underfunded |
| Series B | >6-month sales cycle on SMB | Pricing/packaging problem |
| Series C | NRR <100% | Product-market fit eroding or CS broken |
| Growth | Rule of 40 <20 | Efficiency problem; hiring ahead of revenue |
---
*Sources: Sequoia, a16z operating frameworks; First Round Capital COO benchmarks; SaaStr metrics databases; OpenView SaaS benchmarks; Bain operational maturity models.*
FILE:coo-advisor/scripts/okr_tracker.py
#!/usr/bin/env python3
"""
okr_tracker.py — OKR Cascade and Alignment Tracker
Tracks OKR progress from company → department → team level.
Calculates scores, flags at-risk key results, and generates alignment reports.
Scoring: Google's 0.0–1.0 scale (target: 0.6–0.7; hitting 1.0 means goal was too easy)
Usage:
python okr_tracker.py # Runs with sample data
python okr_tracker.py --input okrs.json # Custom OKR data
python okr_tracker.py --input okrs.json --output report.txt
python okr_tracker.py --format json # Machine-readable output
"""
import json
import sys
import argparse
from datetime import datetime, date
from typing import Any
# ---------------------------------------------------------------------------
# Scoring Engine
# ---------------------------------------------------------------------------
# OKR health thresholds (Google-style 0.0–1.0 scale)
SCORE_THRESHOLDS = {
"on_track": 0.70, # Above this: healthy
"at_risk": 0.40, # Between at_risk and on_track: needs attention
# Below at_risk: off track
}
STATUS_LABELS = {
"on_track": "🟢 On Track",
"at_risk": "🟡 At Risk",
"off_track": "🔴 Off Track",
"complete": "✅ Complete",
"not_started": "⬜ Not Started",
}
RISK_LABELS = {
"critical": "🔴 Critical",
"high": "🟠 High",
"medium": "🟡 Medium",
"low": "🟢 Low",
}
def calculate_kr_score(kr: dict) -> float:
"""
Calculate a Key Result's progress score (0.0–1.0).
Supports multiple KR types:
- numeric: current_value / target_value
- percentage: current_pct / target_pct
- milestone: milestone_score (0.0–1.0 provided directly)
- boolean: done (1.0) / not done (0.0)
"""
kr_type = kr.get("type", "numeric")
if kr_type == "boolean":
return 1.0 if kr.get("done", False) else 0.0
elif kr_type == "milestone":
# Milestone KRs have explicit score (0.0–1.0) or count of milestones hit
milestones_total = kr.get("milestones_total", 1)
milestones_hit = kr.get("milestones_hit", 0)
explicit_score = kr.get("score")
if explicit_score is not None:
return max(0.0, min(1.0, float(explicit_score)))
return milestones_hit / milestones_total if milestones_total > 0 else 0.0
elif kr_type == "percentage":
target = kr.get("target_pct", 100)
current = kr.get("current_pct", 0)
baseline = kr.get("baseline_pct", 0)
if target == baseline:
return 0.0
score = (current - baseline) / (target - baseline)
return max(0.0, min(1.0, score))
else: # numeric (default)
target = kr.get("target_value", 0)
current = kr.get("current_value", 0)
baseline = kr.get("baseline_value", 0)
if target == baseline:
return 0.0
# Handle "lower is better" metrics (e.g., churn, response time)
if kr.get("lower_is_better", False):
if current <= target:
return 1.0
improvement = baseline - current
needed = baseline - target
score = improvement / needed if needed != 0 else 0.0
else:
score = (current - baseline) / (target - baseline)
return max(0.0, min(1.0, score))
def get_kr_status(score: float, quarter_progress: float, kr: dict) -> str:
"""
Determine KR status based on score, time elapsed in quarter, and trend.
A KR is at-risk if its score is significantly behind the time elapsed.
E.g., if we're 70% through the quarter but KR is at 30%, it's at risk.
"""
if kr.get("done", False):
return "complete"
# Not started
if score == 0.0 and quarter_progress < 0.1:
return "not_started"
# Check against absolute thresholds
if score >= SCORE_THRESHOLDS["on_track"]:
return "on_track"
# Adjust for time: if we're early in quarter, lower scores are acceptable
adjusted_threshold = SCORE_THRESHOLDS["at_risk"] * (quarter_progress or 0.5)
if score >= max(adjusted_threshold, SCORE_THRESHOLDS["at_risk"]):
return "at_risk"
return "off_track"
def calculate_objective_score(objective: dict, quarter_progress: float) -> dict:
"""
Score an objective based on its key results.
Returns scored objective with KR scores and status.
"""
key_results = objective.get("key_results", [])
if not key_results:
return {**objective, "score": 0.0, "status": "not_started", "key_results_scored": []}
scored_krs = []
for kr in key_results:
score = calculate_kr_score(kr)
status = get_kr_status(score, quarter_progress, kr)
# Calculate time-adjusted gap
expected_score = quarter_progress * 0.85 # Expect 85% of time-proportional progress
gap = expected_score - score
risk_level = _assess_kr_risk(score, status, gap, quarter_progress, kr)
scored_krs.append({
**kr,
"score": round(score, 3),
"score_pct": f"{score * 100:.0f}%",
"status": status,
"status_label": STATUS_LABELS.get(status, status),
"expected_score": round(expected_score, 3),
"gap_vs_expected": round(gap, 3),
"risk_level": risk_level,
"risk_label": RISK_LABELS.get(risk_level, risk_level),
})
# Objective score = weighted average of KR scores
# Weight is explicit in KR data or defaults to equal weight
total_weight = sum(kr.get("weight", 1.0) for kr in key_results)
weighted_score = sum(
kr_scored["score"] * kr.get("weight", 1.0)
for kr_scored, kr in zip(scored_krs, key_results)
)
obj_score = weighted_score / total_weight if total_weight > 0 else 0.0
# Objective status = worst KR status (a chain is only as strong as weakest link)
status_priority = {"off_track": 0, "at_risk": 1, "not_started": 2, "on_track": 3, "complete": 4}
obj_status = min(scored_krs, key=lambda x: status_priority.get(x["status"], 2))["status"]
return {
**objective,
"score": round(obj_score, 3),
"score_pct": f"{obj_score * 100:.0f}%",
"status": obj_status,
"status_label": STATUS_LABELS.get(obj_status, obj_status),
"key_results_scored": scored_krs,
}
def _assess_kr_risk(
score: float,
status: str,
gap: float,
quarter_progress: float,
kr: dict,
) -> str:
"""Assess risk level for a key result."""
if status == "complete" or status == "on_track":
return "low"
weeks_remaining = kr.get("weeks_remaining", max(1, int((1 - quarter_progress) * 13)))
# Critical: off track with <4 weeks left
if status == "off_track" and weeks_remaining <= 4:
return "critical"
# High: significantly behind with limited time
if gap > 0.3 and weeks_remaining <= 6:
return "high"
# High: off track regardless of time
if status == "off_track":
return "high"
# Medium: at risk
if status == "at_risk":
return "medium"
return "low"
# ---------------------------------------------------------------------------
# OKR Cascade and Alignment Analysis
# ---------------------------------------------------------------------------
def build_okr_tree(data: dict, quarter_progress: float) -> dict:
"""
Build scored OKR tree: company → departments → teams.
Returns full hierarchy with scores at every level.
"""
company = data.get("company_okrs", {})
departments = data.get("department_okrs", [])
teams = data.get("team_okrs", [])
# Score company-level OKRs
company_scored = {
"name": company.get("name", "Company"),
"quarter": company.get("quarter", ""),
"objectives": [
calculate_objective_score(obj, quarter_progress)
for obj in company.get("objectives", [])
],
}
# Score department-level OKRs
depts_scored = []
for dept in departments:
dept_objectives = [
calculate_objective_score(obj, quarter_progress)
for obj in dept.get("objectives", [])
]
dept_score = (
sum(o["score"] for o in dept_objectives) / len(dept_objectives)
if dept_objectives else 0.0
)
depts_scored.append({
**dept,
"objectives": dept_objectives,
"overall_score": round(dept_score, 3),
"overall_score_pct": f"{dept_score * 100:.0f}%",
})
# Score team-level OKRs
teams_scored = []
for team in teams:
team_objectives = [
calculate_objective_score(obj, quarter_progress)
for obj in team.get("objectives", [])
]
team_score = (
sum(o["score"] for o in team_objectives) / len(team_objectives)
if team_objectives else 0.0
)
teams_scored.append({
**team,
"objectives": team_objectives,
"overall_score": round(team_score, 3),
"overall_score_pct": f"{team_score * 100:.0f}%",
})
return {
"company": company_scored,
"departments": depts_scored,
"teams": teams_scored,
}
def analyze_alignment(okr_tree: dict) -> dict:
"""
Analyze how team and department OKRs align to company OKRs.
Flags: orphaned OKRs (no company parent), missing coverage (company OKR with no team support).
"""
company_objective_ids = {
obj.get("id") for obj in okr_tree["company"].get("objectives", [])
if obj.get("id")
}
# Collect all alignment references from dept and team OKRs
alignment_map: dict[str, list[str]] = {oid: [] for oid in company_objective_ids}
orphaned = []
all_supporting = []
def check_objectives(objectives: list, owner_name: str, level: str):
for obj in objectives:
supports = obj.get("supports_company_objective_ids", [])
if not supports:
# Check if it's supposed to support something
if obj.get("supports_company_objective_id"):
supports = [obj["supports_company_objective_id"]]
if not supports:
orphaned.append({
"level": level,
"owner": owner_name,
"objective": obj.get("title", obj.get("name", "Unknown")),
"issue": "No link to company objective — may be misaligned or low priority",
})
else:
for cid in supports:
if cid in alignment_map:
alignment_map[cid].append(f"{level}:{owner_name}")
all_supporting.append(cid)
else:
orphaned.append({
"level": level,
"owner": owner_name,
"objective": obj.get("title", obj.get("name", "Unknown")),
"issue": f"References company objective '{cid}' which doesn't exist",
})
for dept in okr_tree["departments"]:
check_objectives(dept["objectives"], dept.get("name", "Unknown Dept"), "Department")
for team in okr_tree["teams"]:
check_objectives(team["objectives"], team.get("name", "Unknown Team"), "Team")
# Find company objectives with no support from below
unsupported = []
for obj in okr_tree["company"].get("objectives", []):
obj_id = obj.get("id")
if obj_id and obj_id not in all_supporting:
unsupported.append({
"objective_id": obj_id,
"objective": obj.get("title", obj.get("name", "Unknown")),
"issue": "No department or team OKR explicitly supports this company objective",
})
coverage_score = (
len(set(all_supporting)) / len(company_objective_ids) * 100
if company_objective_ids else 100
)
return {
"alignment_map": alignment_map,
"orphaned_okrs": orphaned,
"unsupported_company_objectives": unsupported,
"coverage_score_pct": round(coverage_score, 1),
}
def collect_at_risk_krs(okr_tree: dict) -> list[dict]:
"""Collect all at-risk and off-track key results across the full OKR tree."""
at_risk = []
def scan_objectives(objectives: list, owner: str, level: str):
for obj in objectives:
for kr in obj.get("key_results_scored", []):
if kr["status"] in ("at_risk", "off_track"):
at_risk.append({
"level": level,
"owner": owner,
"objective": obj.get("title", obj.get("name", "Unknown")),
"key_result": kr.get("title", kr.get("name", "Unknown")),
"score": kr["score"],
"score_pct": kr["score_pct"],
"status": kr["status"],
"status_label": kr["status_label"],
"risk_level": kr["risk_level"],
"risk_label": kr["risk_label"],
"gap_vs_expected": kr["gap_vs_expected"],
"notes": kr.get("notes", ""),
})
scan_objectives(
okr_tree["company"].get("objectives", []),
okr_tree["company"].get("name", "Company"),
"Company",
)
for dept in okr_tree["departments"]:
scan_objectives(dept["objectives"], dept.get("name", ""), "Department")
for team in okr_tree["teams"]:
scan_objectives(team["objectives"], team.get("name", ""), "Team")
# Sort: off_track before at_risk, then by gap
status_order = {"off_track": 0, "at_risk": 1}
at_risk.sort(key=lambda x: (status_order.get(x["status"], 2), -x.get("gap_vs_expected", 0)))
return at_risk
# ---------------------------------------------------------------------------
# Report Formatter
# ---------------------------------------------------------------------------
def _score_bar(score: float, width: int = 20) -> str:
"""Render a text progress bar for a 0.0–1.0 score."""
filled = round(score * width)
bar = "█" * filled + "░" * (width - filled)
return f"[{bar}] {score * 100:.0f}%"
def format_report(
okr_tree: dict,
alignment: dict,
at_risk_krs: list[dict],
quarter_progress: float,
quarter_label: str,
) -> str:
"""Format full OKR tracking report as plain text."""
lines = []
now = datetime.now().strftime("%Y-%m-%d %H:%M")
company_name = okr_tree["company"].get("name", "Company")
lines.append("=" * 70)
lines.append(f"OKR TRACKING REPORT — {company_name}")
lines.append(f"Quarter: {quarter_label} | Quarter progress: {quarter_progress * 100:.0f}%")
lines.append(f"Generated: {now}")
lines.append("=" * 70)
# --- Executive Summary ---
lines.append("\n📊 EXECUTIVE SUMMARY")
lines.append("-" * 40)
company_objectives = okr_tree["company"].get("objectives", [])
if company_objectives:
company_avg = sum(o["score"] for o in company_objectives) / len(company_objectives)
on_track = sum(1 for o in company_objectives if o["status"] == "on_track")
at_risk = sum(1 for o in company_objectives if o["status"] == "at_risk")
off_track = sum(1 for o in company_objectives if o["status"] == "off_track")
lines.append(f"Company OKR Score: {_score_bar(company_avg)}")
lines.append(f"Objectives: {len(company_objectives)} total — "
f"🟢 {on_track} on track, 🟡 {at_risk} at risk, 🔴 {off_track} off track")
lines.append(f"At-risk KRs (all): {len(at_risk_krs)}")
lines.append(f"Alignment coverage: {alignment['coverage_score_pct']}% of company objectives have team support")
# Overall health assessment
if company_avg >= 0.7:
health = "🟢 HEALTHY — On track for a strong quarter"
elif company_avg >= 0.5:
health = "🟡 CAUTION — Some objectives need attention"
elif company_avg >= 0.3:
health = "🔴 AT RISK — Multiple objectives behind; intervention needed"
else:
health = "🚨 CRITICAL — Quarter in serious jeopardy; executive review required"
lines.append(f"\nOverall Health: {health}")
# --- Company OKRs ---
lines.append("\n\n🏢 COMPANY OKRs")
lines.append("-" * 40)
for obj in company_objectives:
lines.append(f"\n Objective: {obj.get('title', obj.get('name', 'Unknown'))}")
lines.append(f" Owner: {obj.get('owner', 'Unassigned')} | Score: {_score_bar(obj['score'], 15)} {obj['status_label']}")
for kr in obj.get("key_results_scored", []):
risk_marker = f" {kr['risk_label']}" if kr["risk_level"] in ("critical", "high") else ""
lines.append(f"\n KR: {kr.get('title', kr.get('name', 'Unknown'))}")
lines.append(f" Score: {_score_bar(kr['score'], 12)} {kr['status_label']}{risk_marker}")
# Show actual progress
if kr.get("type") == "numeric":
current = kr.get("current_value", "?")
target = kr.get("target_value", "?")
baseline = kr.get("baseline_value", 0)
unit = kr.get("unit", "")
lines.append(f" Progress: {current}{unit} / {target}{unit} (baseline: {baseline}{unit})")
elif kr.get("type") == "percentage":
lines.append(f" Progress: {kr.get('current_pct', '?')}% / {kr.get('target_pct', '?')}%")
elif kr.get("type") == "milestone":
hit = kr.get("milestones_hit", "?")
total = kr.get("milestones_total", "?")
lines.append(f" Milestones: {hit} / {total}")
if kr.get("notes"):
lines.append(f" Note: {kr['notes']}")
# --- Department OKRs ---
lines.append("\n\n🏬 DEPARTMENT OKRs")
lines.append("-" * 40)
for dept in okr_tree["departments"]:
lines.append(f"\n 📁 {dept.get('name', 'Unknown')} | Score: {_score_bar(dept['overall_score'], 15)}")
for obj in dept.get("objectives", []):
lines.append(f"\n Objective: {obj.get('title', obj.get('name', 'Unknown'))}")
lines.append(f" Owner: {obj.get('owner', 'Unassigned')} | {obj['status_label']}")
supports = obj.get("supports_company_objective_ids", [])
if supports:
lines.append(f" Supports: Company Objective(s) {', '.join(supports)}")
for kr in obj.get("key_results_scored", []):
risk_marker = f" {kr['risk_label']}" if kr["risk_level"] in ("critical", "high") else ""
lines.append(f"\n KR: {kr.get('title', kr.get('name', 'Unknown'))}")
lines.append(f" {_score_bar(kr['score'], 10)} {kr['status_label']}{risk_marker}")
# --- Team OKRs ---
if okr_tree["teams"]:
lines.append("\n\n👥 TEAM OKRs")
lines.append("-" * 40)
for team in okr_tree["teams"]:
lines.append(f"\n 📋 {team.get('name', 'Unknown')} | Score: {_score_bar(team['overall_score'], 15)}")
for obj in team.get("objectives", []):
lines.append(f"\n Objective: {obj.get('title', obj.get('name', 'Unknown'))}")
supports = obj.get("supports_company_objective_ids", [])
if supports:
lines.append(f" Supports: {', '.join(supports)}")
for kr in obj.get("key_results_scored", []):
risk_marker = f" {kr['risk_label']}" if kr["risk_level"] in ("critical", "high") else ""
lines.append(
f" • {kr.get('title', kr.get('name', 'Unknown'))}: "
f"{kr['score_pct']} {kr['status_label']}{risk_marker}"
)
# --- At-Risk KRs ---
lines.append("\n\n⚠️ AT-RISK KEY RESULTS (Action Required)")
lines.append("-" * 40)
if not at_risk_krs:
lines.append("✅ No key results currently at risk or off track.")
else:
critical = [kr for kr in at_risk_krs if kr["risk_level"] == "critical"]
high = [kr for kr in at_risk_krs if kr["risk_level"] == "high"]
medium = [kr for kr in at_risk_krs if kr["risk_level"] == "medium"]
for group_label, group in [("🔴 CRITICAL", critical), ("🟠 HIGH", high), ("🟡 MEDIUM", medium)]:
if not group:
continue
lines.append(f"\n{group_label} ({len(group)} items):")
for kr in group:
lines.append(f"\n [{kr['level']}] {kr['owner']}")
lines.append(f" Obj: {kr['objective']}")
lines.append(f" KR: {kr['key_result']}")
lines.append(f" Score: {kr['score_pct']} {kr['status_label']} (gap vs expected: {kr['gap_vs_expected'] * 100:.0f}pp)")
if kr["notes"]:
lines.append(f" Note: {kr['notes']}")
# --- Alignment Report ---
lines.append("\n\n🔗 ALIGNMENT REPORT")
lines.append("-" * 40)
lines.append(f"Alignment coverage: {alignment['coverage_score_pct']}% of company objectives have explicit support\n")
# Show alignment map
lines.append("Company Objective Coverage:")
for obj in company_objectives:
obj_id = obj.get("id", "")
supporters = alignment["alignment_map"].get(obj_id, [])
obj_name = obj.get("title", obj.get("name", obj_id))
count = len(supporters)
marker = "✅" if count > 0 else "⚠️ "
lines.append(f" {marker} [{obj_id}] {obj_name}")
if supporters:
for s in supporters:
lines.append(f" ↑ {s}")
else:
lines.append(f" ↑ (no department or team OKR supports this)")
if alignment["unsupported_company_objectives"]:
lines.append(f"\n⚠️ Unsupported Company Objectives ({len(alignment['unsupported_company_objectives'])}):")
for u in alignment["unsupported_company_objectives"]:
lines.append(f" • [{u['objective_id']}] {u['objective']}")
lines.append(f" → {u['issue']}")
if alignment["orphaned_okrs"]:
lines.append(f"\n⚠️ Orphaned OKRs (not linked to company objectives):")
for o in alignment["orphaned_okrs"]:
lines.append(f" • [{o['level']}] {o['owner']}: {o['objective']}")
lines.append(f" → {o['issue']}")
# --- Recommendations ---
lines.append("\n\n📋 RECOMMENDED ACTIONS")
lines.append("-" * 40)
recs = _generate_recommendations(okr_tree, at_risk_krs, alignment, quarter_progress)
for i, rec in enumerate(recs, 1):
lines.append(f"\n{i}. {rec['title']}")
lines.append(f" {rec['detail']}")
lines.append(f" Owner: {rec['owner']} | When: {rec['when']}")
lines.append("\n" + "=" * 70)
lines.append("END OF REPORT")
lines.append("=" * 70)
return "\n".join(lines)
def _generate_recommendations(
okr_tree: dict,
at_risk_krs: list[dict],
alignment: dict,
quarter_progress: float,
) -> list[dict]:
"""Generate actionable recommendations based on OKR analysis."""
recs = []
# Critical KRs
critical = [kr for kr in at_risk_krs if kr["risk_level"] == "critical"]
if critical:
recs.append({
"title": f"Emergency review: {len(critical)} critical key result(s) need immediate intervention",
"detail": f"Critical KRs: {', '.join(kr['key_result'] for kr in critical[:3])}. "
f"With limited time remaining, these need escalation today.",
"owner": "COO + KR owners",
"when": "This week",
})
# Off-track objectives
off_track_objs = [
o for o in okr_tree["company"].get("objectives", [])
if o["status"] == "off_track"
]
if off_track_objs:
recs.append({
"title": f"Scope reset for {len(off_track_objs)} off-track company objective(s)",
"detail": "When a company objective is off track by mid-quarter, "
"the options are: (1) resource surge, (2) scope reduction, or (3) accept the miss. "
"Choose explicitly — don't let it drift.",
"owner": "CEO + COO",
"when": "Within 1 week",
})
# Alignment gaps
if alignment["coverage_score_pct"] < 80:
recs.append({
"title": "OKR alignment gap — not all company objectives have team support",
"detail": f"Only {alignment['coverage_score_pct']}% of company objectives have explicit team/dept OKRs supporting them. "
"Either add supporting OKRs or acknowledge these objectives are founder-owned.",
"owner": "COO + VPs",
"when": "Next OKR planning cycle",
})
if alignment["orphaned_okrs"]:
recs.append({
"title": f"{len(alignment['orphaned_okrs'])} orphaned OKR(s) with no company objective linkage",
"detail": "Team OKRs that don't connect to company objectives waste capacity. "
"Either link them explicitly or discontinue them.",
"owner": "Team leads + COO",
"when": "OKR review session",
})
# Late quarter: force ranking
if quarter_progress >= 0.67:
at_risk_count = sum(
1 for o in okr_tree["company"].get("objectives", [])
if o["status"] in ("at_risk", "off_track")
)
if at_risk_count > 0:
recs.append({
"title": f"Late quarter: force-rank which at-risk OKRs to save vs. accept as miss",
"detail": f"{at_risk_count} objectives at risk with <{int((1 - quarter_progress) * 13)} weeks left. "
"You cannot save everything. Pick the 1–2 most important and resource them fully. "
"Explicitly accept the others as misses and learn from them.",
"owner": "CEO + COO",
"when": "Immediately",
})
# Measurement gaps
unscored_krs = []
for obj in okr_tree["company"].get("objectives", []):
for kr in obj.get("key_results_scored", []):
if kr["score"] == 0.0 and kr["status"] == "not_started" and quarter_progress > 0.25:
unscored_krs.append(kr.get("title", kr.get("name", "Unknown")))
if unscored_krs:
recs.append({
"title": f"{len(unscored_krs)} key result(s) show no progress past Q1",
"detail": "KRs with zero progress after 25% of quarter has elapsed are either not started, "
"unmeasured, or forgotten. Require owners to update scores this week.",
"owner": "KR owners",
"when": "This week — before next leadership sync",
})
return recs
def format_json_output(okr_tree: dict, alignment: dict, at_risk_krs: list[dict]) -> str:
"""Format analysis as machine-readable JSON."""
return json.dumps(
{
"generated_at": datetime.now().isoformat(),
"company_score": (
sum(o["score"] for o in okr_tree["company"].get("objectives", []))
/ max(1, len(okr_tree["company"].get("objectives", [])))
),
"at_risk_count": len(at_risk_krs),
"alignment_coverage_pct": alignment["coverage_score_pct"],
"objectives": okr_tree["company"].get("objectives", []),
"departments": okr_tree["departments"],
"teams": okr_tree["teams"],
"at_risk_key_results": at_risk_krs,
"alignment": alignment,
},
indent=2,
)
# ---------------------------------------------------------------------------
# Main Entrypoint
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="OKR Cascade and Alignment Tracker — COO Advisor Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--input", "-i", help="Path to JSON OKR data file", default=None)
parser.add_argument("--output", "-o", help="Path to write report (default: stdout)", default=None)
parser.add_argument(
"--format", "-f",
choices=["text", "json"],
default="text",
help="Output format: text (default) or json",
)
parser.add_argument(
"--quarter-progress",
type=float,
default=None,
help="Override quarter progress (0.0–1.0). Default: auto-calculated from quarter dates.",
)
args = parser.parse_args()
if args.input:
try:
with open(args.input, "r") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: Input file not found: {args.input}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
else:
print("No input file specified — running with sample data.\n")
data = SAMPLE_DATA
# Determine quarter progress
if args.quarter_progress is not None:
quarter_progress = args.quarter_progress
else:
quarter_progress = _calculate_quarter_progress(data)
quarter_label = data.get("company_okrs", {}).get("quarter", "Unknown Quarter")
# Run analysis
okr_tree = build_okr_tree(data, quarter_progress)
alignment = analyze_alignment(okr_tree)
at_risk_krs = collect_at_risk_krs(okr_tree)
# Format output
if args.format == "json":
output = format_json_output(okr_tree, alignment, at_risk_krs)
else:
output = format_report(okr_tree, alignment, at_risk_krs, quarter_progress, quarter_label)
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"Report written to: {args.output}")
else:
print(output)
def _calculate_quarter_progress(data: dict) -> float:
"""Auto-calculate quarter progress from start/end dates in data, or default to 0.5."""
q = data.get("company_okrs", {})
start_str = q.get("quarter_start")
end_str = q.get("quarter_end")
if not start_str or not end_str:
return 0.5 # Default to mid-quarter if not specified
try:
start = date.fromisoformat(start_str)
end = date.fromisoformat(end_str)
today = date.today()
total_days = (end - start).days
elapsed_days = (today - start).days
progress = elapsed_days / total_days if total_days > 0 else 0.5
return max(0.0, min(1.0, progress))
except (ValueError, TypeError):
return 0.5
# ---------------------------------------------------------------------------
# Sample Data
# ---------------------------------------------------------------------------
SAMPLE_DATA = {
"company_okrs": {
"name": "AcmeSaaS",
"quarter": "Q1 2025",
"quarter_start": "2025-01-01",
"quarter_end": "2025-03-31",
"objectives": [
{
"id": "CO1",
"title": "Achieve breakout revenue growth",
"owner": "CEO",
"key_results": [
{
"id": "CO1-KR1",
"title": "Reach $5M net new ARR",
"type": "numeric",
"baseline_value": 0,
"current_value": 2800000,
"target_value": 5000000,
"unit": "",
"notes": "Strong January, February softer; pipeline looks better for March",
},
{
"id": "CO1-KR2",
"title": "Achieve 115% NRR",
"type": "percentage",
"baseline_pct": 108,
"current_pct": 110,
"target_pct": 115,
"notes": "Expansion motion improved; churn still elevated in SMB segment",
},
{
"id": "CO1-KR3",
"title": "Close 3 enterprise deals (>$150K ACV)",
"type": "numeric",
"baseline_value": 0,
"current_value": 1,
"target_value": 3,
"unit": " deals",
"notes": "1 closed, 2 in late-stage negotiation",
},
],
},
{
"id": "CO2",
"title": "Build a world-class product that customers love",
"owner": "CPO",
"key_results": [
{
"id": "CO2-KR1",
"title": "Increase feature adoption rate to 65% (% of customers using 3+ core features)",
"type": "percentage",
"baseline_pct": 48,
"current_pct": 52,
"target_pct": 65,
"notes": "Onboarding improvements shipped; adoption curve is moving",
},
{
"id": "CO2-KR2",
"title": "Ship the integration platform (milestone)",
"type": "milestone",
"milestones_total": 4,
"milestones_hit": 1,
"milestones": [
"API design complete",
"Internal alpha",
"Beta with 5 customers",
"GA launch",
],
"notes": "API design shipped. Internal alpha delayed 2 weeks.",
},
{
"id": "CO2-KR3",
"title": "NPS score reaches 45",
"type": "numeric",
"baseline_value": 32,
"current_value": 38,
"target_value": 45,
"unit": "",
},
],
},
{
"id": "CO3",
"title": "Build an operationally excellent company",
"owner": "COO",
"key_results": [
{
"id": "CO3-KR1",
"title": "Reduce burn multiple from 1.8x to 1.3x",
"type": "numeric",
"baseline_value": 1.8,
"current_value": 1.65,
"target_value": 1.3,
"lower_is_better": True,
"unit": "x",
},
{
"id": "CO3-KR2",
"title": "Achieve <30-day customer onboarding (avg)",
"type": "numeric",
"baseline_value": 47,
"current_value": 38,
"target_value": 30,
"lower_is_better": True,
"unit": " days",
"notes": "Good progress; blocked by technical setup step (avg 12 days)",
},
{
"id": "CO3-KR3",
"title": "Voluntary attrition <10%",
"type": "numeric",
"baseline_value": 15,
"current_value": 12,
"target_value": 10,
"lower_is_better": True,
"unit": "%",
"notes": "2 unexpected departures in January; retention initiatives launched",
},
],
},
],
},
"department_okrs": [
{
"name": "Sales",
"owner": "VP Sales",
"objectives": [
{
"title": "Drive net new ARR to hit company growth target",
"owner": "VP Sales",
"supports_company_objective_ids": ["CO1"],
"key_results": [
{
"title": "Close $4M in new business ARR",
"type": "numeric",
"baseline_value": 0,
"current_value": 2200000,
"target_value": 4000000,
"unit": "",
},
{
"title": "Maintain pipeline coverage ratio ≥3x",
"type": "numeric",
"baseline_value": 2.5,
"current_value": 3.1,
"target_value": 3.0,
"unit": "x",
},
{
"title": "Reduce average sales cycle to 42 days",
"type": "numeric",
"baseline_value": 58,
"current_value": 50,
"target_value": 42,
"lower_is_better": True,
"unit": " days",
},
],
}
],
},
{
"name": "Engineering",
"owner": "VP Engineering",
"objectives": [
{
"title": "Deliver the integration platform on schedule",
"owner": "VP Engineering",
"supports_company_objective_ids": ["CO2"],
"key_results": [
{
"title": "Integration platform beta live with 5 customers",
"type": "milestone",
"milestones_total": 3,
"milestones_hit": 1,
"notes": "Alpha delayed — dependency on API gateway refactor",
},
{
"title": "Deploy frequency ≥10/week",
"type": "numeric",
"baseline_value": 6,
"current_value": 9,
"target_value": 10,
"unit": "/week",
},
{
"title": "P0/P1 incidents <2 per month",
"type": "numeric",
"baseline_value": 5,
"current_value": 2.5,
"target_value": 2,
"lower_is_better": True,
"unit": "/month",
},
],
}
],
},
{
"name": "Customer Success",
"owner": "VP CS",
"objectives": [
{
"title": "Drive retention and expansion to fuel NRR growth",
"owner": "VP CS",
"supports_company_objective_ids": ["CO1", "CO2"],
"key_results": [
{
"title": "Gross retention ≥92%",
"type": "percentage",
"baseline_pct": 88,
"current_pct": 89,
"target_pct": 92,
"notes": "3 at-risk accounts in red status",
},
{
"title": "Average onboarding time ≤30 days",
"type": "numeric",
"baseline_value": 47,
"current_value": 38,
"target_value": 30,
"lower_is_better": True,
"unit": " days",
},
{
"title": "Expansion ARR from existing customers: $800K",
"type": "numeric",
"baseline_value": 0,
"current_value": 580000,
"target_value": 800000,
"unit": "",
},
],
}
],
},
],
"team_okrs": [
{
"name": "Platform Engineering",
"department": "Engineering",
"objectives": [
{
"title": "Build the integration API infrastructure",
"supports_company_objective_ids": ["CO2"],
"key_results": [
{
"title": "API gateway v2 deployed to production",
"type": "boolean",
"done": False,
"notes": "Targeting end of week 8",
},
{
"title": "Webhook system handles 10K events/sec",
"type": "boolean",
"done": False,
},
{
"title": "P99 API latency <200ms",
"type": "numeric",
"baseline_value": 380,
"current_value": 290,
"target_value": 200,
"lower_is_better": True,
"unit": "ms",
},
],
}
],
},
{
"name": "Enterprise Sales Team",
"department": "Sales",
"objectives": [
{
"title": "Land 3 enterprise accounts",
"supports_company_objective_ids": ["CO1"],
"key_results": [
{
"title": "3 enterprise deals closed",
"type": "numeric",
"baseline_value": 0,
"current_value": 1,
"target_value": 3,
"unit": " deals",
},
{
"title": "5 enterprise POCs initiated",
"type": "numeric",
"baseline_value": 0,
"current_value": 4,
"target_value": 5,
"unit": " POCs",
},
],
}
],
},
],
}
if __name__ == "__main__":
main()
FILE:coo-advisor/scripts/ops_efficiency_analyzer.py
#!/usr/bin/env python3
"""
ops_efficiency_analyzer.py — Operational Efficiency Analyzer
Analyzes startup operational efficiency using Theory of Constraints,
process maturity scoring, and bottleneck identification.
Usage:
python ops_efficiency_analyzer.py # Runs with sample data
python ops_efficiency_analyzer.py --input data.json # Custom data
python ops_efficiency_analyzer.py --input data.json --output report.txt
Input format: See SAMPLE_DATA at bottom of file.
"""
import json
import sys
import argparse
import math
from datetime import datetime
from typing import Any, Optional
# ---------------------------------------------------------------------------
# Data Models (plain dicts with type aliases for clarity)
# ---------------------------------------------------------------------------
ProcessData = dict[str, Any]
TeamData = dict[str, Any]
MetricsData = dict[str, Any]
# ---------------------------------------------------------------------------
# Process Maturity Scoring
# ---------------------------------------------------------------------------
MATURITY_LEVELS = {
1: "Ad Hoc",
2: "Defined",
3: "Managed",
4: "Optimized",
5: "Innovating",
}
MATURITY_DESCRIPTIONS = {
1: "No documented process. Outcomes depend on individual heroics.",
2: "Process exists and is documented. Inconsistently followed.",
3: "Process is followed consistently. Metrics are tracked.",
4: "Process is optimized based on metrics. Proactively improved.",
5: "Process enables competitive advantage. Continuously innovating.",
}
MATURITY_CRITERIA = {
"documentation": {
"weight": 0.20,
"levels": {
0: "No documentation",
1: "Informal notes or tribal knowledge",
2: "Process documented but not maintained",
3: "Documented, current, accessible",
4: "Documented with examples, edge cases, and owner",
5: "Living doc with version history and improvement log",
},
},
"ownership": {
"weight": 0.15,
"levels": {
0: "No owner",
1: "Unclear ownership, multiple people responsible",
2: "Named team responsible",
3: "Named individual DRI",
4: "DRI with metrics accountability",
5: "DRI with improvement mandate and resources",
},
},
"metrics": {
"weight": 0.20,
"levels": {
0: "No metrics",
1: "Anecdotal measurement",
2: "Some metrics tracked, not regularly reviewed",
3: "Key metrics tracked and reviewed monthly",
4: "Metrics drive decisions, targets set",
5: "Predictive metrics, benchmarked externally",
},
},
"automation": {
"weight": 0.20,
"levels": {
0: "100% manual",
1: "Mostly manual, some tools used",
2: "Key steps automated, significant manual work remains",
3: "Majority automated, manual exception handling",
4: "Mostly automated with exception playbooks",
5: "Fully automated with human oversight only",
},
},
"consistency": {
"weight": 0.15,
"levels": {
0: "Never consistent",
1: "Consistent <50% of time",
2: "Consistent 50-75% of time",
3: "Consistent 75-90% of time",
4: "Consistent >90% of time",
5: "Six Sigma level (>99.7%)",
},
},
"feedback_loop": {
"weight": 0.10,
"levels": {
0: "No feedback loop",
1: "Ad hoc complaints surface issues",
2: "Periodic review when problems arise",
3: "Regular review cadence",
4: "Structured improvement cycles",
5: "Real-time feedback with automated triggers",
},
},
}
def score_process_maturity(process: ProcessData) -> dict[str, Any]:
"""
Score a single process on 1-5 maturity scale.
Returns scored process with dimension breakdown and recommendations.
"""
maturity_inputs = process.get("maturity", {})
total_score = 0.0
dimension_scores = {}
recommendations = []
for dimension, config in MATURITY_CRITERIA.items():
raw_score = maturity_inputs.get(dimension, 0)
# Normalize raw score (0-5) to weight
normalized = (raw_score / 5.0) * config["weight"] * 5
total_score += normalized
dimension_scores[dimension] = raw_score
# Generate recommendation if below threshold
if raw_score < 3:
severity = "🔴 Critical" if raw_score < 2 else "🟡 Needs work"
recommendations.append({
"dimension": dimension,
"current_score": raw_score,
"target_score": 3,
"severity": severity,
"action": _get_improvement_action(dimension, raw_score),
})
# Clamp to 1-5 range (scores can't be below 1 for a running process)
maturity_score = max(1.0, min(5.0, total_score))
maturity_level = round(maturity_score)
return {
"name": process["name"],
"maturity_score": round(maturity_score, 2),
"maturity_level": maturity_level,
"maturity_label": MATURITY_LEVELS[maturity_level],
"dimension_scores": dimension_scores,
"recommendations": recommendations,
"process_data": process,
}
def _get_improvement_action(dimension: str, current_score: int) -> str:
"""Return a concrete improvement action for a given dimension and score."""
actions = {
"documentation": {
0: "Write a basic SOP this week: trigger, steps, owner, done-definition",
1: "Convert tribal knowledge into a written process doc with clear steps",
2: "Assign a process owner to maintain and update documentation quarterly",
},
"ownership": {
0: "Assign a DRI (Directly Responsible Individual) today",
1: "Clarify ownership: assign one named person, remove ambiguity",
2: "Give the named owner accountability for process metrics",
},
"metrics": {
0: "Define 1-2 metrics that measure if this process is working",
1: "Set up automated metric collection and add to monthly review",
2: "Set targets for each metric and review monthly",
},
"automation": {
0: "Identify the highest-volume manual step; automate it first",
1: "Run automation ROI calc — if payback <12 months, build it",
2: "Automate exception routing and error notifications",
},
"consistency": {
0: "Root-cause why the process fails; fix the #1 failure mode",
1: "Create a checklist for the process; require sign-off",
2: "Add process adherence check to team's weekly review",
},
"feedback_loop": {
0: "Add this process to monthly operational review agenda",
1: "Create a feedback channel (Slack thread, form) for process issues",
2: "Set a quarterly review date for this process",
},
}
return actions.get(dimension, {}).get(current_score, "Improve this dimension")
# ---------------------------------------------------------------------------
# Bottleneck Analysis (Theory of Constraints)
# ---------------------------------------------------------------------------
def analyze_bottlenecks(processes: list[ProcessData]) -> dict[str, Any]:
"""
Identify bottlenecks using throughput analysis.
Bottleneck = step with lowest throughput (or highest queue buildup).
"""
bottlenecks = []
throughput_chain = []
for process in processes:
steps = process.get("steps", [])
if not steps:
continue
step_analysis = []
min_throughput = float("inf")
bottleneck_step = None
for step in steps:
throughput = step.get("throughput_per_day", 0)
queue_depth = step.get("current_queue", 0)
avg_wait_hours = step.get("avg_wait_hours", 0)
# Utilization estimate
capacity = step.get("capacity_per_day", throughput * 1.2)
utilization = (throughput / capacity * 100) if capacity > 0 else 100
step_info = {
"name": step["name"],
"throughput_per_day": throughput,
"queue_depth": queue_depth,
"avg_wait_hours": avg_wait_hours,
"utilization_pct": round(utilization, 1),
"is_bottleneck": False,
}
step_analysis.append(step_info)
if throughput < min_throughput:
min_throughput = throughput
bottleneck_step = step_info
if bottleneck_step:
bottleneck_step["is_bottleneck"] = True
# Calculate flow efficiency
total_lead_time = sum(
s.get("avg_wait_hours", 0) + s.get("avg_process_hours", 1)
for s in steps
)
total_process_time = sum(s.get("avg_process_hours", 1) for s in steps)
flow_efficiency = (
(total_process_time / total_lead_time * 100)
if total_lead_time > 0
else 0
)
bottlenecks.append({
"process": process["name"],
"bottleneck_step": bottleneck_step["name"],
"bottleneck_throughput": min_throughput,
"bottleneck_queue": bottleneck_step["queue_depth"],
"flow_efficiency_pct": round(flow_efficiency, 1),
"steps": step_analysis,
"toc_recommendation": _generate_toc_recommendation(
bottleneck_step, process
),
})
throughput_chain.append({
"process": process["name"],
"steps": step_analysis,
})
# Rank bottlenecks by severity (queue depth × utilization)
for b in bottlenecks:
b["severity_score"] = b["bottleneck_queue"] * (b["bottleneck_throughput"] or 1)
bottlenecks.sort(key=lambda x: x["severity_score"], reverse=True)
return {
"bottlenecks": bottlenecks,
"throughput_chain": throughput_chain,
}
def _generate_toc_recommendation(bottleneck_step: dict, process: ProcessData) -> str:
"""Generate a Theory of Constraints recommendation for a bottleneck."""
util = bottleneck_step["utilization_pct"]
queue = bottleneck_step["queue_depth"]
step_name = bottleneck_step["name"]
if util >= 90:
return (
f"ELEVATE: '{step_name}' is at {util}% utilization — at capacity. "
f"Add resources (people, automation, or parallel processing) immediately. "
f"Queue of {queue} units will grow until capacity is increased."
)
elif util >= 70:
return (
f"EXPLOIT: '{step_name}' has capacity headroom but is the constraint. "
f"Eliminate non-value-add work in this step. Protect it from interruptions. "
f"Ensure upstream steps feed it steadily, not in batches."
)
else:
return (
f"INVESTIGATE: '{step_name}' shows low throughput ({bottleneck_step['throughput_per_day']}/day) "
f"despite available capacity. Root cause may be upstream blocking, "
f"unclear handoffs, or quality issues requiring rework."
)
# ---------------------------------------------------------------------------
# Team Structure Analysis
# ---------------------------------------------------------------------------
def analyze_team_structure(team: TeamData) -> dict[str, Any]:
"""
Analyze team structure for span of control, layer count, and hiring gaps.
"""
issues = []
recommendations = []
warnings = []
total_headcount = team.get("total_headcount", 0)
departments = team.get("departments", [])
# Span of control analysis
span_issues = []
for dept in departments:
for manager in dept.get("managers", []):
direct_reports = manager.get("direct_reports", 0)
manages_managers = manager.get("manages_managers", False)
optimal_min = 3 if manages_managers else 5
optimal_max = 5 if manages_managers else 8
if direct_reports < optimal_min:
span_issues.append({
"manager": manager["name"],
"dept": dept["name"],
"reports": direct_reports,
"issue": "Under-span",
"recommendation": f"Merge team or promote ICs — {direct_reports} reports is management overhead",
})
elif direct_reports > optimal_max:
span_issues.append({
"manager": manager["name"],
"dept": dept["name"],
"reports": direct_reports,
"issue": "Over-span",
"recommendation": f"Split team — {direct_reports} reports means minimal 1:1 time and poor feedback loops",
})
# Management layers analysis
max_layers = team.get("management_layers", 0)
expected_layers = _expected_layers(total_headcount)
if max_layers > expected_layers + 1:
issues.append({
"type": "Over-layered",
"detail": f"{max_layers} management layers for {total_headcount} people. "
f"Expected: {expected_layers}. Excess layers slow decisions.",
"recommendation": "Flatten: remove middle management layers that don't add decision value",
})
# Revenue per employee by department
annual_revenue = team.get("annual_revenue_usd", 0)
dept_analysis = []
for dept in departments:
headcount = dept.get("headcount", 0)
if headcount > 0 and annual_revenue > 0:
rev_per_employee = annual_revenue / headcount
benchmark = _dept_revenue_benchmark(dept["name"], team.get("stage", "series_a"))
efficiency_pct = (rev_per_employee / benchmark * 100) if benchmark > 0 else None
dept_analysis.append({
"department": dept["name"],
"headcount": headcount,
"revenue_per_employee": round(rev_per_employee),
"benchmark": benchmark,
"efficiency_vs_benchmark_pct": round(efficiency_pct, 1) if efficiency_pct else "N/A",
"status": _efficiency_status(efficiency_pct),
})
# Open req health
open_reqs = team.get("open_requisitions", 0)
req_to_headcount_ratio = (open_reqs / total_headcount * 100) if total_headcount > 0 else 0
if req_to_headcount_ratio > 20:
warnings.append(
f"High open req ratio: {open_reqs} open reqs against {total_headcount} headcount "
f"({req_to_headcount_ratio:.0f}%). This level of hiring while operating is operationally disruptive."
)
return {
"total_headcount": total_headcount,
"management_layers": max_layers,
"expected_layers": expected_layers,
"span_of_control_issues": span_issues,
"structural_issues": issues,
"department_efficiency": dept_analysis,
"open_req_health": {
"open_reqs": open_reqs,
"ratio_pct": round(req_to_headcount_ratio, 1),
"warnings": warnings,
},
}
def _expected_layers(headcount: int) -> int:
if headcount <= 15:
return 1
elif headcount <= 50:
return 2
elif headcount <= 150:
return 3
elif headcount <= 500:
return 4
else:
return 5
def _dept_revenue_benchmark(dept_name: str, stage: str) -> int:
"""Revenue per employee benchmark by department and stage (USD)."""
benchmarks = {
"series_a": {
"engineering": 400000,
"sales": 250000,
"customer_success": 300000,
"marketing": 500000,
"operations": 400000,
"product": 400000,
"default": 200000,
},
"series_b": {
"engineering": 500000,
"sales": 350000,
"customer_success": 400000,
"marketing": 700000,
"operations": 500000,
"product": 500000,
"default": 300000,
},
"series_c": {
"engineering": 600000,
"sales": 450000,
"customer_success": 500000,
"marketing": 900000,
"operations": 600000,
"product": 600000,
"default": 400000,
},
}
stage_data = benchmarks.get(stage, benchmarks["series_a"])
dept_key = dept_name.lower().replace(" ", "_").replace("-", "_")
return stage_data.get(dept_key, stage_data["default"])
def _efficiency_status(efficiency_pct: Optional[float]) -> str:
if efficiency_pct is None:
return "N/A"
if efficiency_pct >= 90:
return "🟢 On benchmark"
elif efficiency_pct >= 70:
return "🟡 Below benchmark"
else:
return "🔴 Significantly below"
# ---------------------------------------------------------------------------
# Improvement Plan Generator
# ---------------------------------------------------------------------------
def generate_improvement_plan(
process_scores: list[dict],
bottleneck_analysis: dict,
team_analysis: dict,
metrics: MetricsData,
) -> list[dict]:
"""
Generate a prioritized improvement plan combining all analysis outputs.
Priority = Impact × Urgency / Effort
"""
items = []
# Priority 1: Process bottlenecks (Theory of Constraints — fix the constraint first)
for b in bottleneck_analysis.get("bottlenecks", [])[:3]:
items.append({
"priority": 1,
"category": "Bottleneck",
"item": f"Resolve bottleneck in '{b['process']}' at step '{b['bottleneck_step']}'",
"detail": b["toc_recommendation"],
"impact": "HIGH — constraint limits entire system throughput",
"effort": "MEDIUM",
"owner_suggestion": "COO + process owner",
"timebox": "2-4 weeks",
"success_metric": f"Throughput at {b['bottleneck_step']} increases by 25%+",
})
# Priority 2: Critical process maturity gaps
critical_processes = [
p for p in process_scores if p["maturity_score"] < 2.0
]
for proc in sorted(critical_processes, key=lambda x: x["maturity_score"]):
for rec in proc["recommendations"][:2]: # Top 2 recs per critical process
items.append({
"priority": 2,
"category": "Process Maturity",
"item": f"Fix {rec['dimension']} in '{proc['name']}' (score: {rec['current_score']}/5)",
"detail": rec["action"],
"impact": "HIGH — ad-hoc processes create inconsistency and risk",
"effort": "LOW-MEDIUM",
"owner_suggestion": "Process owner",
"timebox": "1-2 weeks",
"success_metric": f"Dimension score improves to 3/5",
})
# Priority 3: Team structural issues
for issue in team_analysis.get("structural_issues", []):
items.append({
"priority": 3,
"category": "Org Structure",
"item": issue["type"],
"detail": issue["detail"],
"impact": "MEDIUM — structural issues compound over time",
"effort": "HIGH",
"owner_suggestion": "COO + People",
"timebox": "1-2 quarters",
"success_metric": "Management layer count normalized",
})
for span_issue in team_analysis.get("span_of_control_issues", []):
severity = "HIGH" if span_issue["issue"] == "Over-span" else "MEDIUM"
items.append({
"priority": 3,
"category": "Span of Control",
"item": f"{span_issue['issue']}: {span_issue['manager']} ({span_issue['dept']})",
"detail": span_issue["recommendation"],
"impact": severity,
"effort": "MEDIUM",
"owner_suggestion": f"VP {span_issue['dept']}",
"timebox": "1 quarter",
"success_metric": "Span within 5-8 for ICs, 3-5 for managers",
})
# Priority 4: Maturity improvements for non-critical processes
medium_processes = [
p for p in process_scores if 2.0 <= p["maturity_score"] < 3.5
]
for proc in sorted(medium_processes, key=lambda x: x["maturity_score"])[:3]:
if proc["recommendations"]:
top_rec = proc["recommendations"][0]
items.append({
"priority": 4,
"category": "Process Improvement",
"item": f"Improve {top_rec['dimension']} in '{proc['name']}'",
"detail": top_rec["action"],
"impact": "MEDIUM",
"effort": "LOW",
"owner_suggestion": "Process owner",
"timebox": "2-4 weeks",
"success_metric": f"Dimension score reaches 3/5",
})
# Priority 5: Metrics-driven flags
burn_multiple = metrics.get("burn_multiple")
if burn_multiple and burn_multiple > 2.0:
items.append({
"priority": 2,
"category": "Financial Efficiency",
"item": f"Burn multiple of {burn_multiple:.1f}x is above healthy range",
"detail": "Burn multiple >1.5x indicates spending exceeds efficient growth. Review headcount-to-revenue ratio by department.",
"impact": "HIGH",
"effort": "MEDIUM",
"owner_suggestion": "COO + CFO",
"timebox": "30 days to diagnose, 60-90 days to act",
"success_metric": "Burn multiple <1.5x within 2 quarters",
})
nrr = metrics.get("net_revenue_retention_pct")
if nrr and nrr < 100:
items.append({
"priority": 1,
"category": "Revenue Health",
"item": f"NRR of {nrr}% — losing more from churn/contraction than gaining from expansion",
"detail": "NRR <100% means the customer base shrinks without new sales. Investigate churn root causes immediately.",
"impact": "CRITICAL",
"effort": "HIGH",
"owner_suggestion": "COO + VP CS",
"timebox": "Immediate — 30 days to root cause, 90 days to fix",
"success_metric": "NRR >100% within 2 quarters",
})
# Sort by priority then impact
priority_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
items.sort(key=lambda x: (x["priority"], priority_order.get(x["impact"].split(" — ")[0], 9)))
return items
# ---------------------------------------------------------------------------
# Report Formatter
# ---------------------------------------------------------------------------
def format_report(
process_scores: list[dict],
bottleneck_analysis: dict,
team_analysis: dict,
improvement_plan: list[dict],
metrics: MetricsData,
) -> str:
"""Format the full analysis report as plain text."""
lines = []
now = datetime.now().strftime("%Y-%m-%d %H:%M")
lines.append("=" * 70)
lines.append("OPERATIONAL EFFICIENCY ANALYSIS REPORT")
lines.append(f"Generated: {now}")
lines.append("=" * 70)
# --- Executive Summary ---
lines.append("\n📊 EXECUTIVE SUMMARY")
lines.append("-" * 40)
avg_maturity = (
sum(p["maturity_score"] for p in process_scores) / len(process_scores)
if process_scores else 0
)
critical_count = sum(1 for p in process_scores if p["maturity_score"] < 2.0)
bottleneck_count = len(bottleneck_analysis.get("bottlenecks", []))
plan_items = len(improvement_plan)
lines.append(f"Average Process Maturity: {avg_maturity:.1f}/5.0 ({MATURITY_LEVELS.get(round(avg_maturity), 'Unknown')})")
lines.append(f"Critical Process Gaps: {critical_count}")
lines.append(f"Active Bottlenecks: {bottleneck_count}")
lines.append(f"Improvement Plan Items: {plan_items}")
if metrics:
lines.append("\nKey Business Metrics:")
if metrics.get("burn_multiple"):
flag = " ⚠️" if metrics["burn_multiple"] > 2.0 else ""
lines.append(f" Burn Multiple: {metrics['burn_multiple']:.1f}x{flag}")
if metrics.get("net_revenue_retention_pct"):
flag = " ⚠️" if metrics["net_revenue_retention_pct"] < 100 else ""
lines.append(f" NRR: {metrics['net_revenue_retention_pct']}%{flag}")
if metrics.get("cac_payback_months"):
flag = " ⚠️" if metrics["cac_payback_months"] > 18 else ""
lines.append(f" CAC Payback: {metrics['cac_payback_months']} months{flag}")
# --- Process Maturity Scores ---
lines.append("\n\n📋 PROCESS MATURITY SCORES")
lines.append("-" * 40)
lines.append(f"{'Process':<35} {'Score':>6} {'Level':<12} {'Status'}")
lines.append(f"{'─'*35} {'─'*6} {'─'*12} {'─'*20}")
for p in sorted(process_scores, key=lambda x: x["maturity_score"]):
score = p["maturity_score"]
label = p["maturity_label"]
status = "🔴 Critical" if score < 2 else ("🟡 Needs work" if score < 3.5 else "🟢 Healthy")
lines.append(f"{p['name']:<35} {score:>6.1f} {label:<12} {status}")
# Dimension heatmap
lines.append("\n\nDimension Breakdown (scores 0-5):")
lines.append(f"{'Process':<30} {'Doc':>4} {'Own':>4} {'Met':>4} {'Aut':>4} {'Con':>4} {'Fbk':>4}")
lines.append(f"{'─'*30} {'─'*4} {'─'*4} {'─'*4} {'─'*4} {'─'*4} {'─'*4}")
for p in sorted(process_scores, key=lambda x: x["maturity_score"]):
d = p["dimension_scores"]
lines.append(
f"{p['name']:<30} {d.get('documentation',0):>4} {d.get('ownership',0):>4} "
f"{d.get('metrics',0):>4} {d.get('automation',0):>4} "
f"{d.get('consistency',0):>4} {d.get('feedback_loop',0):>4}"
)
# --- Bottleneck Analysis ---
lines.append("\n\n🔍 BOTTLENECK ANALYSIS (Theory of Constraints)")
lines.append("-" * 40)
bottlenecks = bottleneck_analysis.get("bottlenecks", [])
if not bottlenecks:
lines.append("No process steps defined for bottleneck analysis.")
else:
for i, b in enumerate(bottlenecks, 1):
lines.append(f"\n{i}. {b['process']}")
lines.append(f" Bottleneck step: {b['bottleneck_step']}")
lines.append(f" Throughput: {b['bottleneck_throughput']}/day")
lines.append(f" Queue depth: {b['bottleneck_queue']} units")
lines.append(f" Flow efficiency: {b['flow_efficiency_pct']}%")
lines.append(f" Recommendation: {b['toc_recommendation']}")
lines.append(f"\n Step-by-step throughput:")
for step in b["steps"]:
marker = " ← BOTTLENECK" if step["is_bottleneck"] else ""
lines.append(
f" {step['name']:<30} {step['throughput_per_day']:>4}/day "
f"Queue: {step['queue_depth']:>4} Util: {step['utilization_pct']:>5.1f}%{marker}"
)
# --- Team Structure ---
lines.append("\n\n👥 TEAM STRUCTURE ANALYSIS")
lines.append("-" * 40)
lines.append(f"Total headcount: {team_analysis['total_headcount']}")
lines.append(f"Management layers: {team_analysis['management_layers']} (expected: {team_analysis['expected_layers']})")
span_issues = team_analysis.get("span_of_control_issues", [])
if span_issues:
lines.append(f"\n⚠️ Span of Control Issues ({len(span_issues)}):")
for issue in span_issues:
lines.append(f" {issue['issue']}: {issue['manager']} ({issue['dept']}) — {issue['reports']} reports")
lines.append(f" → {issue['recommendation']}")
dept_eff = team_analysis.get("department_efficiency", [])
if dept_eff:
lines.append(f"\nDepartment Revenue Efficiency:")
lines.append(f"{'Department':<20} {'HC':>4} {'Rev/Head':>10} {'Benchmark':>10} {'vs Bench':>9} {'Status'}")
lines.append(f"{'─'*20} {'─'*4} {'─'*10} {'─'*10} {'─'*9} {'─'*20}")
for d in dept_eff:
rev = f"," if d['revenue_per_employee'] else "N/A"
bench = f"," if d['benchmark'] else "N/A"
vs_bench = f"{d['efficiency_vs_benchmark_pct']}%" if d['efficiency_vs_benchmark_pct'] != "N/A" else "N/A"
lines.append(
f"{d['department']:<20} {d['headcount']:>4} {rev:>10} {bench:>10} {vs_bench:>9} {d['status']}"
)
# --- Improvement Plan ---
lines.append("\n\n🎯 PRIORITIZED IMPROVEMENT PLAN")
lines.append("-" * 40)
lines.append("Items ranked by priority (1=highest). Fix Priority 1 before starting Priority 2.\n")
current_priority = None
for i, item in enumerate(improvement_plan, 1):
if item["priority"] != current_priority:
current_priority = item["priority"]
lines.append(f"\nPRIORITY {current_priority}")
lines.append("─" * 30)
lines.append(f"\n{i}. [{item['category']}] {item['item']}")
lines.append(f" Detail: {item['detail']}")
lines.append(f" Impact: {item['impact']}")
lines.append(f" Effort: {item['effort']}")
lines.append(f" Owner: {item['owner_suggestion']}")
lines.append(f" Timebox: {item['timebox']}")
lines.append(f" Success: {item['success_metric']}")
lines.append("\n" + "=" * 70)
lines.append("END OF REPORT")
lines.append("=" * 70)
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Main Entrypoint
# ---------------------------------------------------------------------------
def run_analysis(data: dict) -> str:
"""Run the full analysis pipeline on input data."""
processes = data.get("processes", [])
team = data.get("team", {})
metrics = data.get("metrics", {})
# 1. Score process maturity
process_scores = [score_process_maturity(p) for p in processes]
# 2. Analyze bottlenecks
bottleneck_analysis = analyze_bottlenecks(processes)
# 3. Analyze team structure
team_analysis = analyze_team_structure(team)
# 4. Generate improvement plan
improvement_plan = generate_improvement_plan(
process_scores, bottleneck_analysis, team_analysis, metrics
)
# 5. Format and return report
return format_report(
process_scores, bottleneck_analysis, team_analysis, improvement_plan, metrics
)
def main():
parser = argparse.ArgumentParser(
description="Operational Efficiency Analyzer — COO Advisor Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--input", "-i",
help="Path to JSON input file (default: use built-in sample data)",
default=None,
)
parser.add_argument(
"--output", "-o",
help="Path to write report (default: stdout)",
default=None,
)
args = parser.parse_args()
if args.input:
try:
with open(args.input, "r") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: Input file not found: {args.input}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in input file: {e}", file=sys.stderr)
sys.exit(1)
else:
print("No input file specified — running with sample data.\n")
data = SAMPLE_DATA
report = run_analysis(data)
if args.output:
with open(args.output, "w") as f:
f.write(report)
print(f"Report written to: {args.output}")
else:
print(report)
# ---------------------------------------------------------------------------
# Sample Data
# ---------------------------------------------------------------------------
SAMPLE_DATA = {
"company": "AcmeSaaS",
"stage": "series_b",
"metrics": {
"annual_revenue_usd": 18000000,
"burn_multiple": 1.8,
"net_revenue_retention_pct": 108,
"cac_payback_months": 14,
"headcount": 85,
"monthly_churn_pct": 1.2,
},
"processes": [
{
"name": "Customer Onboarding",
"category": "Customer Success",
"maturity": {
"documentation": 3,
"ownership": 4,
"metrics": 3,
"automation": 2,
"consistency": 3,
"feedback_loop": 2,
},
"steps": [
{
"name": "Contract signed → kickoff scheduled",
"throughput_per_day": 4,
"capacity_per_day": 6,
"current_queue": 3,
"avg_wait_hours": 4,
"avg_process_hours": 1,
},
{
"name": "Technical setup & integration",
"throughput_per_day": 2,
"capacity_per_day": 3,
"current_queue": 8,
"avg_wait_hours": 24,
"avg_process_hours": 8,
},
{
"name": "Training & enablement",
"throughput_per_day": 3,
"capacity_per_day": 4,
"current_queue": 2,
"avg_wait_hours": 8,
"avg_process_hours": 4,
},
{
"name": "Go-live confirmation",
"throughput_per_day": 4,
"capacity_per_day": 6,
"current_queue": 1,
"avg_wait_hours": 2,
"avg_process_hours": 1,
},
],
},
{
"name": "Sales Deal Qualification",
"category": "Sales",
"maturity": {
"documentation": 2,
"ownership": 3,
"metrics": 4,
"automation": 2,
"consistency": 2,
"feedback_loop": 3,
},
"steps": [
{
"name": "Inbound lead review",
"throughput_per_day": 15,
"capacity_per_day": 20,
"current_queue": 5,
"avg_wait_hours": 2,
"avg_process_hours": 0.5,
},
{
"name": "BANT qualification call",
"throughput_per_day": 8,
"capacity_per_day": 10,
"current_queue": 12,
"avg_wait_hours": 24,
"avg_process_hours": 1,
},
{
"name": "Demo scheduling & prep",
"throughput_per_day": 6,
"capacity_per_day": 8,
"current_queue": 4,
"avg_wait_hours": 8,
"avg_process_hours": 0.5,
},
],
},
{
"name": "Engineering Deployment",
"category": "Engineering",
"maturity": {
"documentation": 4,
"ownership": 5,
"metrics": 4,
"automation": 4,
"consistency": 5,
"feedback_loop": 4,
},
"steps": [
{
"name": "PR submitted",
"throughput_per_day": 20,
"capacity_per_day": 25,
"current_queue": 8,
"avg_wait_hours": 3,
"avg_process_hours": 2,
},
{
"name": "Code review",
"throughput_per_day": 18,
"capacity_per_day": 22,
"current_queue": 10,
"avg_wait_hours": 4,
"avg_process_hours": 1,
},
{
"name": "CI pipeline",
"throughput_per_day": 18,
"capacity_per_day": 30,
"current_queue": 2,
"avg_wait_hours": 0.5,
"avg_process_hours": 0.5,
},
{
"name": "Deploy to production",
"throughput_per_day": 16,
"capacity_per_day": 20,
"current_queue": 1,
"avg_wait_hours": 0.5,
"avg_process_hours": 0.25,
},
],
},
{
"name": "Incident Response",
"category": "Engineering / Operations",
"maturity": {
"documentation": 2,
"ownership": 2,
"metrics": 1,
"automation": 1,
"consistency": 2,
"feedback_loop": 1,
},
"steps": [],
},
{
"name": "Employee Onboarding",
"category": "People",
"maturity": {
"documentation": 2,
"ownership": 2,
"metrics": 1,
"automation": 1,
"consistency": 2,
"feedback_loop": 2,
},
"steps": [],
},
{
"name": "Vendor Procurement",
"category": "Operations",
"maturity": {
"documentation": 1,
"ownership": 1,
"metrics": 0,
"automation": 0,
"consistency": 1,
"feedback_loop": 0,
},
"steps": [],
},
],
"team": {
"total_headcount": 85,
"annual_revenue_usd": 18000000,
"stage": "series_b",
"management_layers": 3,
"open_requisitions": 18,
"departments": [
{
"name": "Engineering",
"headcount": 32,
"managers": [
{"name": "VP Engineering", "direct_reports": 4, "manages_managers": True},
{"name": "Engineering Manager (Platform)", "direct_reports": 7, "manages_managers": False},
{"name": "Engineering Manager (Product)", "direct_reports": 8, "manages_managers": False},
{"name": "Engineering Manager (Infra)", "direct_reports": 9, "manages_managers": False},
],
},
{
"name": "Sales",
"headcount": 18,
"managers": [
{"name": "VP Sales", "direct_reports": 3, "manages_managers": True},
{"name": "Sales Manager (SMB)", "direct_reports": 6, "manages_managers": False},
{"name": "Sales Manager (Enterprise)", "direct_reports": 4, "manages_managers": False},
],
},
{
"name": "Customer Success",
"headcount": 12,
"managers": [
{"name": "VP CS", "direct_reports": 2, "manages_managers": False},
],
},
{
"name": "Marketing",
"headcount": 8,
"managers": [
{"name": "VP Marketing", "direct_reports": 7, "manages_managers": False},
],
},
{
"name": "Operations",
"headcount": 6,
"managers": [
{"name": "COO", "direct_reports": 5, "manages_managers": True},
],
},
{
"name": "Product",
"headcount": 9,
"managers": [
{"name": "VP Product", "direct_reports": 8, "manages_managers": False},
],
},
],
},
}
if __name__ == "__main__":
main()
FILE:cpo-advisor/SKILL.md
---
name: "cpo-advisor"
description: "Product leadership for scaling companies. Product vision, portfolio strategy, product-market fit, and product org design. Use when setting product vision, managing a product portfolio, measuring PMF, designing product teams, prioritizing at the portfolio level, reporting to the board on product, or when user mentions CPO, product strategy, product-market fit, product organization, portfolio prioritization, or roadmap strategy."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: cpo-leadership
updated: 2026-03-05
python-tools: pmf_scorer.py, portfolio_analyzer.py
frameworks: pmf-playbook, product-strategy, product-org-design
---
# CPO Advisor
Strategic product leadership. Vision, portfolio, PMF, org design. Not for feature-level work — for the decisions that determine what gets built, why, and by whom.
## Keywords
CPO, chief product officer, product strategy, product vision, product-market fit, PMF, portfolio management, product org, roadmap strategy, product metrics, north star metric, retention curve, product trio, team topologies, Jobs to be Done, category design, product positioning, board product reporting, invest-maintain-kill, BCG matrix, switching costs, network effects
## Quick Start
### Score Your Product-Market Fit
```bash
python scripts/pmf_scorer.py
```
Multi-dimensional PMF score across retention, engagement, satisfaction, and growth.
### Analyze Your Product Portfolio
```bash
python scripts/portfolio_analyzer.py
```
BCG matrix classification, investment recommendations, portfolio health score.
## The CPO's Core Responsibilities
The CPO owns three things. Everything else is delegation.
| Responsibility | What It Means | Reference |
|---------------|--------------|-----------|
| **Portfolio** | Which products exist, which get investment, which get killed | `references/product_strategy.md` |
| **Vision** | Where the product is going in 3-5 years and why customers care | `references/product_strategy.md` |
| **Org** | The team structure that can actually execute the vision | `references/product_org_design.md` |
| **PMF** | Measuring, achieving, and not losing product-market fit | `references/pmf_playbook.md` |
| **Metrics** | North star → leading → lagging hierarchy, board reporting | This file |
## Diagnostic Questions
These questions expose whether you have a strategy or a list.
**Portfolio:**
- Which product is the dog? Are you killing it or lying to yourself?
- If you had to cut 30% of your portfolio tomorrow, what stays?
- What's your portfolio's combined D30 retention? Is it trending up?
**PMF:**
- What's your retention curve for your best cohort?
- What % of users would be "very disappointed" if your product disappeared?
- Is organic growth happening without you pushing it?
**Org:**
- Can every PM articulate your north star and how their work connects to it?
- When did your last product trio do user interviews together?
- What's blocking your slowest team — the people or the structure?
**Strategy:**
- If you could only ship one thing this quarter, what is it and why?
- What's your moat in 12 months? In 3 years?
- What's the riskiest assumption in your current product strategy?
## Product Metrics Hierarchy
```
North Star Metric (1, owned by CPO)
↓ explains changes in
Leading Indicators (3-5, owned by PMs)
↓ eventually become
Lagging Indicators (revenue, churn, NPS)
```
**North Star rules:** One number. Measures customer value delivered, not revenue. Every team can influence it.
**Good North Stars by business model:**
| Model | North Star Example |
|-------|------------------|
| B2B SaaS | Weekly active accounts using core feature |
| Consumer | D30 retained users |
| Marketplace | Successful transactions per week |
| PLG | Accounts reaching "aha moment" within 14 days |
| Data product | Queries run per active user per week |
### The CPO Dashboard
| Category | Metric | Frequency |
|----------|--------|-----------|
| Growth | North star metric | Weekly |
| Growth | D30 / D90 retention by cohort | Weekly |
| Acquisition | New activations | Weekly |
| Activation | Time to "aha moment" | Weekly |
| Engagement | DAU/MAU ratio | Weekly |
| Satisfaction | NPS trend | Monthly |
| Portfolio | Revenue per product | Monthly |
| Portfolio | Engineering investment % per product | Monthly |
| Moat | Feature adoption depth | Monthly |
## Investment Postures
Every product gets one: **Invest / Maintain / Kill**. "Wait and see" is not a posture — it's a decision to lose share.
| Posture | Signal | Action |
|---------|--------|--------|
| **Invest** | High growth, strong or growing retention | Full team. Aggressive roadmap. |
| **Maintain** | Stable revenue, slow growth, good margins | Bug fixes only. Milk it. |
| **Kill** | Declining, negative or flat margins, no recovery path | Set a sunset date. Write a migration plan. |
## Red Flags
**Portfolio:**
- Products that have been "question marks" for 2+ quarters without a decision
- Engineering capacity allocated to your highest-revenue product but your highest-growth product is understaffed
- More than 30% of team time on products with declining revenue
**PMF:**
- You have to convince users to keep using the product
- Support requests are mostly "how do I do X" rather than "I want X to also do Y"
- D30 retention is below 20% (consumer) or 40% (B2B) and not improving
**Org:**
- PMs writing specs and handing to design, who hands to engineering (waterfall in agile clothing)
- Platform team has a 6-week queue for stream-aligned team requests
- CPO has not talked to a real customer in 30+ days
**Metrics:**
- North star going up while retention is going down (metric is wrong)
- Teams optimizing their own metrics at the expense of company metrics
- Roadmap built from sales requests, not user behavior data
## Integration with Other C-Suite Roles
| When... | CPO works with... | To... |
|---------|-------------------|-------|
| Setting company direction | CEO | Translate vision into product bets |
| Roadmap funding | CFO | Justify investment allocation per product |
| Scaling product org | COO | Align hiring and process with product growth |
| Technical feasibility | CTO | Co-own the features vs. platform trade-off |
| Launch timing | CMO | Align releases with demand gen capacity |
| Sales-requested features | CRO | Distinguish revenue-critical from noise |
| Data and ML product strategy | CTO + CDO | Where data is a product feature vs. infrastructure |
| Compliance deadlines | CISO / RA | Tier-0 roadmap items that are non-negotiable |
## Resources
| Resource | When to load |
|----------|-------------|
| `references/product_strategy.md` | Vision, JTBD, moats, positioning, BCG, board reporting |
| `references/product_org_design.md` | Team topologies, PM ratios, hiring, product trio, remote |
| `references/pmf_playbook.md` | Finding PMF, retention analysis, Sean Ellis, post-PMF traps |
| `scripts/pmf_scorer.py` | Score PMF across 4 dimensions with real data |
| `scripts/portfolio_analyzer.py` | BCG classify and score your product portfolio |
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- Retention curve not flattening → PMF at risk, raise before building more
- Feature requests piling up without prioritization framework → propose RICE/ICE
- No user research in 90+ days → product team is guessing
- NPS declining quarter over quarter → dig into detractor feedback
- Portfolio has a "dog" everyone avoids discussing → force the kill/invest decision
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Do we have PMF?" | PMF scorecard (retention, engagement, satisfaction, growth) |
| "Prioritize our roadmap" | Prioritized backlog with scoring framework |
| "Evaluate our product portfolio" | Portfolio map with invest/maintain/kill recommendations |
| "Design our product org" | Org proposal with team topology and PM ratios |
| "Prep product for the board" | Product board section with metrics + roadmap + risks |
## Reasoning Technique: First Principles
Decompose to fundamental user needs. Question every assumption about what customers want. Rebuild from validated evidence, not inherited roadmaps.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:cpo-advisor/references/pmf_playbook.md
# PMF Playbook
How to find product-market fit, measure it, and not lose it. Steps, not theory.
---
## What PMF Actually Is
PMF is when a product pulls users in rather than pushing them. Signals:
- Users find the product without you telling them about it
- They're upset when it doesn't work
- They bring their colleagues, their friends, their boss
- They build workarounds when a feature is missing
PMF is not:
- Users saying they like it
- A good NPS score with flat growth
- Enterprise customers who are locked in but churning at contract end
---
## Step 1: Find Your Best Customers First
Before measuring PMF across everyone, find the segment where PMF is strongest.
**How:**
1. Export a list of all churned users and all retained users (D90+)
2. Identify 5-10 attributes to compare: company size, industry, job title, signup source, first action taken, time to first value
3. Find the attributes that are over-represented in retained vs. churned
4. That's your highest-PMF segment
**This is not an analytics project.** Call 10 retained power users. Ask:
- "What were you doing before you found us?"
- "What would you use if we shut down tomorrow?"
- "Who else in your life has this problem?"
The segment where this conversation is easy and the answers are specific — that's where your PMF is.
---
## Step 2: Measure the Three PMF Signals
Run all three. They measure different things. One signal without the others is misleading.
### Signal 1: Retention Curves
**Method:**
1. Cohort users by week or month of first use
2. Calculate % still active at D1, D7, D14, D30, D60, D90
3. Plot the curve for each cohort
**Interpretation:**
| Curve Shape | What It Means |
|-------------|--------------|
| Drops to zero | No PMF. Product doesn't solve a recurring problem. |
| Drops and keeps dropping | Weak PMF. Some people find value, but not enough to keep coming back. |
| Drops then flattens above 0 | PMF signal. A core group finds ongoing value. |
| Flattens higher with each newer cohort | PMF improving. You're learning. |
**Benchmarks:**
| Segment | D30 Retention (PMF threshold) | D90 Retention (strong PMF) |
|---------|-------------------------------|---------------------------|
| Consumer | > 20% | > 10% |
| SMB SaaS | > 40% | > 25% |
| Enterprise SaaS | > 60% | > 45% |
| Marketplace (buyers) | > 30% | > 20% |
| PLG (free-to-paid) | > 25% free D30, > 50% paid D30 | > 15% free D90 |
**If retention is below threshold:**
- Don't run more acquisition. You'll just churn faster.
- Find the users who ARE retained. Understand why. Build for them.
---
### Signal 2: Sean Ellis Test
Survey users with one question: "How would you feel if you could no longer use [Product]?"
**Answers:**
- Very disappointed
- Somewhat disappointed
- Not disappointed (it really isn't that useful)
- N/A — I no longer use [Product]
**Scoring:**
- Count only "very disappointed" responses
- Divide by total non-churned respondents
- PMF threshold: **> 40% "very disappointed"**
**Sample size requirement:** Minimum 40 responses. Under 40, the signal is noisy.
**When to run it:**
- When you have 100-500 active users
- Quarterly for ongoing tracking
- After major product changes
**What to do with "somewhat disappointed":**
Don't lump them with "very disappointed." The delta between "somewhat" and "very" is where your retention problem lives. Interview people in the "somewhat" group. What's missing? Why only somewhat?
**When score is 20-35%:** You have a segment with PMF. Find them. Ask what they love. Run a separate survey for just that segment.
**When score is < 20%:** Your core value proposition isn't working. This is not a retention tactics problem. Revisit the fundamental problem you're solving.
---
### Signal 3: Organic Growth and Referral
**Metric:** % of new signups that came from existing user referral, word of mouth, or organic search — without a paid incentive.
**Threshold:** > 20% of new users are coming organically without incentive programs.
**How to measure:**
1. Tag signup source: paid, organic search, referral (with referral code), direct/dark social
2. Track monthly. Is the organic % trending up or stable?
3. Interview organic signups: "How did you hear about us?" (don't trust the dropdown)
**Why this matters:** Paid growth can mask the absence of PMF. You can buy users who churn. You can't buy users who tell their friends.
---
## Step 3: Run PMF Experiments (Pre-PMF)
If you're below thresholds, don't optimize — experiment. The goal is to find the version of the product where at least a small segment has PMF.
### The PMF Experiment Loop
```
1. Pick one customer segment + one hypothesis about their job to be done
2. Remove everything from the product that doesn't serve that job
3. Run a 4-week cohort with only that segment
4. Measure retention + Sean Ellis for that cohort
5. If PMF signal: this is your beachhead. Double down.
If no signal: new hypothesis. Repeat.
```
**Time box:** Each experiment 4-8 weeks. If you're running experiments for 18+ months with no signal, revisit the problem space, not just the solution.
### What to Change
| Lever | Change | Expected Impact |
|-------|--------|-----------------|
| Target segment | Narrow ICP from "all companies" to "Series A SaaS" | Faster learning, higher retention |
| Core job | Reframe from feature-benefit to outcome-benefit | Better product decisions |
| Onboarding | Remove steps to time-to-value | D1 retention up |
| Pricing | Move from per-seat to per-outcome | Align incentives with value |
| Channel | Switch from outbound to PLG | Different segment discovers product |
---
## Step 4: Validate PMF (Post-Signal, Pre-Scale)
Congratulations, you have a retention curve that flattens. Before you scale:
**Validate that it's real:**
- Can you acquire more of the same customers? (Test CAC at 2x current volume)
- Do the retained users expand? (Are they buying more seats, upgrading?)
- Is the NPS from retained users > 40?
- Are they forgiving of bugs and slowness? (Love, not tolerance)
**Validate the unit economics:**
- LTV / CAC > 3x (for SaaS)
- Payback period < 18 months
- Gross margin > 60% (SaaS), > 40% (marketplace)
**The danger zone:** Convincing yourself you have PMF before economics are viable. High retention with terrible unit economics is not a business — it's a hobby that grows.
---
## PMF by Business Model
### B2B SaaS
**Primary signal:** D90 retention > 45% in target segment.
**Secondary signals:**
- NPS from retained users > 50
- Expansion revenue from retained accounts (NRR > 110%)
- Sales cycle shortening as word-of-mouth increases
**PMF finding strategy:**
- Start with one vertical, not the whole market
- Get 3-5 reference customers who use it daily and refer others
- Don't expand segment until you can replicate the reference case
**Common false signals:**
- Retained users who are locked in by contract, not value
- Expansion revenue from upselling, not from organic growth
- High satisfaction survey scores with flat usage data
---
### B2C / Consumer
**Primary signal:** D30 retention > 20%, with a flat or rising tail at D90.
**Secondary signals:**
- DAU/MAU ratio > 20% (daily habit product: > 40%)
- Session depth (users exploring multiple features, not one-and-done)
- Organic referral rate > 20% of new installs
**PMF finding strategy:**
- Consumer PMF is about habit formation — which behavior do you own in a user's day?
- Find the "aha moment" (the action that predicts retention). Build everything to get users there faster.
- Segment ruthlessly — consumer PMF is often strong in one demographic, weak in others.
**Common false signals:**
- High D1 retention from email campaigns that re-engage dormant users
- Good NPS from vocal users who are power users, not typical users
- Media buzz driving installs from wrong audience
---
### Marketplace
**Primary signal:** Successful transaction rate and repeat buyer rate.
**Secondary signals:**
- Supply-side retention (sellers/providers coming back)
- Liquidity score: % of demand requests matched within acceptable time
- Referral: both sides sending others
**PMF challenge:** You have two customers (supply and demand). PMF can exist on one side and not the other.
**PMF finding strategy:**
- Start with constrained geography or category — don't try to be national before local works
- Measure GMV per cohort, not just transaction count
- Find the "magic moment" for both buyer and seller. Optimize for both.
---
### PLG (Product-Led Growth)
**Primary signal:** Free-to-paid conversion rate + paid retention.
**Secondary signals:**
- Time to activation (reaching the "aha moment" in free tier)
- PQL (product-qualified lead) conversion to paid
- Team invites from individual users (virality coefficient)
**PMF finding strategy:**
- The free tier must have genuine value — not a crippled trial
- Track activation milestone (the action that predicts conversion)
- Optimize activation before conversion — conversion optimizations don't work if nobody activates
---
## After PMF: The Scaling Trap
Most companies that fail after PMF weren't ready to scale. They scaled the wrong thing.
### The Scaling Trap
You have PMF with segment A. You hire sales and start selling to segment B. Segment B doesn't retain. NPS drops. Engineers chase segment B feature requests. Segment A users feel abandoned.
**This is the most common way early-stage companies die after PMF.**
### What to Do After PMF
**First 90 days after confirming PMF:**
1. Document your best customer profile in extreme detail
2. Build the playbook to replicate the reference customer, not to expand the ICP
3. Hire sales to replicate, not to expand
4. Instrument everything — you need to know what's driving retention for every new cohort
5. Don't launch new features. Remove friction from the path that's already working.
**The expansion question:** Only expand ICP when:
- You can replicate the reference customer at 3x volume with same retention
- CAC is declining (word of mouth in the reference segment)
- You've exhausted density in the reference segment
**Don't expand ICP to save the business.** Expanding ICP when retention is declining is panic, not strategy.
---
## How to Know When PMF Is Slipping
PMF is not a binary state. It can degrade. Watch for:
| Signal | What's Happening | Response |
|--------|-----------------|----------|
| D30 retention declining across cohorts | Product changes or market change are eroding value | Run Sean Ellis test immediately. Interview churned users. |
| Sean Ellis score dropping | Users less passionate about the product | Feature gap opening. Competitive pressure. |
| NPS dropping for retained users | Power users seeing degraded experience | Product quality or performance issues. |
| Organic referral rate declining | Satisfied users less enthusiastic | Product becoming commoditized. Moat eroding. |
| Support tickets shifting from feature requests to bug reports | Technical debt catching up | Engineering quality investment needed. |
| Sales cycles lengthening | ICP no longer self-evident. Positioning drift. | Re-run positioning exercise. Sharpen ICP. |
**The PMF quarterly check:**
Run Sean Ellis test every quarter. Track D30 retention by cohort every month. Put both on the CPO dashboard. These are your vital signs.
---
## Quick Reference
| Test | Threshold | Frequency |
|------|-----------|-----------|
| Sean Ellis | > 40% very disappointed | Quarterly |
| D30 retention (B2B SaaS) | > 40% | Monthly (by cohort) |
| D30 retention (consumer) | > 20% | Monthly (by cohort) |
| D90 retention (B2B SaaS) | > 45% | Monthly (by cohort) |
| Organic signup % | > 20% | Monthly |
| NPS (retained users) | > 40 | Quarterly |
| DAU/MAU (if daily product) | > 20% | Weekly |
Use `scripts/pmf_scorer.py` to run all dimensions together with weighted scoring.
FILE:cpo-advisor/references/product_org_design.md
# Product Org Design Reference
How to structure, hire, and run product organizations at different stages. No generic advice — stage-specific, role-specific, and honest about what breaks.
---
## 1. Team Topologies for Product Orgs
Matthew Skelton and Manuel Pais defined four team types. Here's how they map to product organizations.
### Four Team Types
#### Stream-Aligned Teams
Own a continuous flow of customer-facing work. They take problems all the way from discovery to delivery to measurement.
**Product org equivalent:** Feature teams, growth teams, customer journey teams.
**Characteristics:**
- Long-lived (not project teams)
- Full-stack: PM + Designer + 3-7 Engineers + QA
- Can deploy independently without asking another team
- Own their backlog, their metrics, their outcomes
**Health signals:**
- Ships without waiting on other teams more than 20% of the time
- Can define their own north star and trace it to company metric
- PMs spend > 50% of time in discovery, not coordination
**Warning signs:**
- Every sprint has "dependencies" blocking progress
- Team has PMs but engineers don't know the customer problems
- Roadmap is handed to them, not co-created
#### Platform Teams
Build and maintain shared capabilities so stream-aligned teams don't reinvent them.
**Product org equivalent:** Platform product team, internal tools, shared infrastructure.
**Characteristics:**
- Serve internal customers (other teams), not end users directly
- Measure success by stream-aligned team velocity, not feature count
- Self-service is the goal — stream teams should be unblocked without filing tickets
**Health signals:**
- Stream-aligned teams can do 80% of their work without filing a ticket to platform
- Platform has a public API and documentation, not just engineers who know how it works
- Platform team metrics include "number of teams using X without assistance"
**Warning signs:**
- Platform team has a 6-week SLA for new features
- Stream teams fork the platform to avoid waiting
- Platform team's backlog is driven by platform's own ideas, not stream team pain
**The platform product manager role:**
Platform PMs are not feature PMs. They manage internal customers. Key skills:
- Developer experience empathy (they're building for engineers)
- API and infrastructure intuition (you can't PM what you don't understand)
- Saying "no" gracefully when requests are misuses of the platform
#### Enabling Teams
Temporarily help other teams upskill in a domain. Not permanent.
**Product org equivalent:** UX research team, data literacy evangelism, accessibility experts.
**Duration:** Time-boxed. 3-6 months. Then they leave and the skill stays.
**Failure mode:** Enabling teams that never leave become coordination bottlenecks.
#### Complicated Subsystem Teams
Deep expertise required. Minimal interaction.
**Product org equivalent:** ML/AI product team, compliance product, payments, internationalization engine.
**Characteristics:**
- Specialists who can't be split across stream-aligned teams
- Interact via well-defined interface, not collaboration
- Have their own PM who understands the domain deeply
---
## 2. Org Models at Each Stage
### Pre-Seed / Seed (1-20 engineers)
**Structure:** Founder/CEO or founder/CTO is the PM. Maybe one hired PM at 15+ engineers.
**Don't build:** Process, specialization, hierarchy.
**Do build:** Direct customer access, fast iteration loops, written learning from every experiment.
**PM role at this stage:**
- Not shipping features. Talking to customers.
- Not writing specs. Running experiments.
- Not managing engineers. Being managed alongside them.
**Hiring mistake:** Hiring a "process PM" who builds Jira templates before you have PMF.
---
### Series A (20-60 engineers)
**Structure:** 2-4 PMs, organized by product area or customer journey.
```
CPO / Head of Product
├── PM — Core Product (the thing customers pay for)
├── PM — Growth / Acquisition (how more customers get there)
└── PM — Platform (as soon as engineering says they need it)
```
**What you add:** One embedded designer. Analytics shared.
**First PM hire criteria:**
- Has shipped something users use, not just wrote a spec
- Comfortable with ambiguity and no process
- Will talk to customers without being asked
- Understands the technical constraints intuitively
**What breaks at Series A:**
- Verbal communication stops working. First thing to document: the roadmap, the north star, who decided what.
- Engineers start asking "why are we building this?" — good. Answer it.
- Customer requests multiply faster than capacity. You need a prioritization framework.
---
### Series B (60-150 engineers)
**Structure:** 4-8 PMs, head of product, first design hire, embedded or dedicated analytics.
```
CPO
├── Head of Product
│ ├── PM — [Team 1] (stream-aligned)
│ ├── PM — [Team 2] (stream-aligned)
│ ├── PM — [Team 3] (stream-aligned)
│ └── PM — Platform (if engineering > 40)
├── Head of Design (or Senior Designer × 2-3)
└── Analytics (shared, or 1 embedded per team)
```
**What you add at Series B:**
- Head of Product (frees CPO from backlog, runs PM team)
- First Head of Design hire (if not already)
- Dedicated growth team (PLG or acquisition)
**What breaks at Series B:**
- PMs start optimizing their own team's metrics instead of company metrics
- Design and engineering don't talk until sprint planning
- Data team is a ticket queue — PMs can't self-serve
**Fix:** OKR alignment across teams. Design in discovery, not in handoff. Analytics tool self-serve access for every PM.
---
### Series C (150-400 engineers)
**Structure:** 8-15 PMs, multiple PM leads / directors, specialized functions.
```
CPO
├── VP / Director of Product
│ ├── PM Lead — [Product Line 1]
│ │ ├── PM
│ │ └── PM
│ ├── PM Lead — [Product Line 2]
│ │ ├── PM
│ │ └── PM
│ └── PM Lead — Platform
├── Head of Design
│ ├── UX Design
│ ├── Product Design
│ └── UX Research
├── Head of Data / Analytics
│ ├── Product Analytics
│ └── Data Science
└── Head of Product Operations
```
**What you add at Series C:**
- PM leads / directors (PMs managing PMs)
- Dedicated UX research
- Head of Product Operations (roadmap tooling, PM hiring, analytics standards, product community)
- Possible Chief of Staff (Product)
**What breaks at Series C:**
- Coordination overhead becomes the primary job
- PMs become project managers managing handoffs instead of product decisions
- Consistency across teams: 5 different ways to write a spec, 5 different analytics setups
- CPO loses touch with customers
**Fix:** Product principles (written, opinionated, used in reviews). Embedded researchers. Regular CPO customer calls (monthly minimum). Product ops to solve consistency without bureaucracy.
---
## 3. PM:Engineer Ratios
### By Stage
| Stage | Engineers | PMs | Ratio | Notes |
|-------|-----------|-----|-------|-------|
| Seed | 5 | 0-1 | 1:5 | Founder PM common |
| Series A | 20-40 | 2-4 | 1:8 | First real PMs |
| Series B | 60-100 | 5-8 | 1:10 | Platform PM emerges |
| Series C | 150-250 | 12-18 | 1:12 | PM leads required |
| Growth | 300+ | 20+ | 1:12-15 | Specialization high |
### By Team Type
| Team Type | Ratio | Rationale |
|-----------|-------|-----------|
| Stream-aligned (feature) | 1:6-8 | High discovery work, many stakeholders |
| Growth / PLG | 1:8-10 | High experimentation, more autonomy per engineer |
| Platform | 1:10-15 | Lower ambiguity, more self-directed engineers |
| Complicated subsystem (ML, payments) | 1:12-20 | Technical direction from engineers, PM is translator |
**The ratio trap:** These are guidelines, not targets. A great PM in a bad org with 12 engineers accomplishes less than a great PM with 8 in a healthy org. Fix the org before optimizing the ratio.
---
## 4. When to Hire Key Roles
### Head of Design
**Not yet signal:**
- Fewer than 2 full-time designers
- Product is primarily technical (API-first, developer tool with no GUI)
- Design is consistently described as "not a blocker"
**Hire now signal:**
- Design has become a coordination problem (who reviews what? which system? what's the standard?)
- You have 3+ designers and they're inconsistent
- CPO is spending significant time on design decisions
- Customers cite UX as a blocker to adoption
**What this person does:**
- Builds and maintains the design system
- Runs UX research as a function, not one-off projects
- Hires and grows the design team
- Keeps designers from becoming pixel-pushers and keeps them in discovery
**Wrong hire:** A senior IC who can't build process and isn't excited about it.
---
### Head of Data / Analytics
**Not yet signal:**
- < 5 PMs, data team shared with engineering
- You don't have product analytics instrumentation yet (worry about that first)
- Product metrics are reviewed monthly and nobody acts on them
**Hire now signal:**
- PMs are filing tickets for basic metric questions (sign that data team is a bottleneck)
- Multiple products with different tracking setups — no common definitions
- You want to run experiments but don't have infrastructure
- Leadership is making product decisions without data (not from choice — from access)
**What this person does:**
- Defines the event taxonomy and enforces it
- Builds self-serve analytics capability for PMs
- Runs A/B testing infrastructure
- Partners with PMs on experiment design (before launch, not after)
**Wrong hire:** A pure data scientist who can't build product analytics infrastructure and doesn't want to.
---
### Head of Product Operations
**Hire when you have:**
- 8+ PMs with inconsistent processes
- CPO spending > 30% of time on internal coordination
- No standard for roadmap tools, prioritization, or PM onboarding
- Product team can't answer "what are all teams working on this quarter?" without a 2-hour meeting
**What this person does:**
- PM onboarding and development program
- Roadmap and tooling standards (Jira, Linear, Notion — pick one and enforce it)
- Data pipelines from product to leadership (weekly metrics, OKR tracking)
- PM hiring and interview process
- Voice of product org in cross-functional coordination
**What this person does NOT do:**
- Drive product strategy (that's the CPO)
- Manage PMs (that's the Head of Product or PM leads)
- Own analytics (that's Head of Data)
---
## 5. The Product Trio
Every product team should have three roles working together from day one of discovery:
```
Product Manager → What to build and why
Product Designer → How users experience it
Tech Lead / Engineer → How to build it sustainably
```
### How the Trio Actually Works
**Discovery (weeks 1-2 of any new initiative):**
- All three in user interviews together
- All three reviewing competitive products
- All three in problem framing sessions
- Output: Opportunity, not solution
**Ideation (days):**
- All three generating solutions
- Designer prototypes 2-3 options
- Engineer provides feasibility gut check on each
- PM synthesizes against strategy
- Output: Prototype for testing
**Testing (days):**
- Designer and PM run tests (engineer optional but encouraged)
- Tests with 5-8 real customers
- All three review findings together
- Output: Decision: build, iterate, or kill
**Delivery (sprints):**
- PM writes acceptance criteria (what done looks like from user perspective)
- Engineer owns implementation
- Designer owns QA for experience quality
- All three do final review before release
### Trio Anti-Patterns
| Anti-Pattern | What It Looks Like | Why It Fails |
|-------------|-------------------|--------------|
| **PM → Designer → Engineer** | Waterfall disguised as agile | Late discovery of infeasibility and poor UX |
| **Engineer-led** | Engineers propose solutions, PM and designer polish | Builds technically correct thing nobody wants |
| **PM-led dictation** | PM writes detailed spec, team executes | Team has no context, can't make good trade-offs |
| **Designer detached** | Designers design in isolation, present to engineers | Beautiful mockup that's 8x harder to build than alternative |
| **No research** | Trio invents problems and solutions in a conference room | Building for themselves |
---
## 6. Remote vs. Co-located Product Teams
The debate is mostly settled. Here's what actually matters:
### What Changes with Remote
| Activity | Co-located | Remote | Fix |
|----------|-----------|--------|-----|
| Discovery sync | Organic, hallway | Requires scheduling | Daily async standups + weekly sync |
| Whiteboarding | Easy | Friction | Figma, Miro — async-first artifacts |
| Design review | Walk over | Calendar invite | Record reviews; written decisions |
| Relationship building | Osmotic | Deliberate | Regular 1:1s, team rituals, offsites |
| Onboarding | Shadow in person | Document-heavy | Written playbooks + buddy system |
| Difficult conversations | Easier in person | Harder | Default to video, not Slack |
### The Async-First Product Team
Works well remote IF:
- Decisions are written (Notion, Confluence, not Slack threads)
- Roadmaps are accessible to everyone without a meeting
- Product reviews are recorded and linked
- Discovery artifacts are shared before the meeting, discussed in the meeting
- 1:1s are weekly and actual (not "let's skip this week")
**What doesn't survive async:**
- Ambiguous ownership
- Verbal agreements (write it down or it didn't happen)
- Teams where "PM wrote the spec" is the only documentation
### Remote Product Org Practices
**Weekly Cadence:**
```
Monday: Async kickoff — each team posts week's focus + blockers
Tuesday: Product trio sync (30 min, per team)
Wednesday: CPO / Head of Product 1:1s
Thursday: Cross-team PM sync (30 min, rotating topics)
Friday: Async retrospective notes + week summary
```
**Monthly:**
- Full product org sync (all PMs, designers, heads)
- CPO product review (each team presents one initiative)
- Metrics review (company + team level)
**Quarterly:**
- In-person or virtual offsite
- Strategy and OKR setting
- Individual growth conversations
---
## Quick Reference
| Stage | Structure | First Hire Priority |
|-------|-----------|-------------------|
| Seed | Founder PM | Generalist PM with customer instincts |
| Series A | 2-3 PMs, flat | First real PM, owns a product area |
| Series B | Head of Product, 4-8 PMs | Head of Design |
| Series C | Org layers, PM leads | Head of Data + Product Ops |
| Growth | Full specialization | Chief of Staff (Product) |
**PM:Engineer ratio target by stage:**
Seed 1:5 → Series A 1:8 → Series B 1:10 → Series C 1:12 → Growth 1:15
**Three things that fix most product org problems:**
1. Stream-aligned teams with full-stack ownership (PM + Design + Eng)
2. OKRs that cascade from company to team to individual
3. Product trio in discovery, not just delivery
FILE:cpo-advisor/references/product_strategy.md
# Product Strategy Reference
Frameworks for product vision, competitive positioning, portfolio management, and board reporting. No theory — only what CPOs actually use.
---
## 1. Vision Frameworks
### Jobs to Be Done (JTBD)
JTBD is not a feature framework. It's a way to understand *why* customers hire your product and under what circumstances.
**The core insight:** People don't want your product. They want to make progress in their lives, and they hire your product to help. When you understand the job, you understand competition differently.
#### Conducting JTBD Interviews
**Who to interview:** Recent buyers and recent churners. Not power users — they're already converted.
**The interview script (condensed):**
```
1. "Walk me through the last time you [started using / stopped using] this product."
2. "What were you doing the day before you decided?"
3. "What else did you consider?"
4. "What almost stopped you from doing it?"
5. "Now that you're using it, what does your day look like differently?"
```
**What you're extracting:**
- **Functional job:** What task are they accomplishing?
- **Emotional job:** How do they feel during and after?
- **Social job:** How are they perceived?
- **Timeline:** What triggered the switch? (the "push" from old solution + "pull" toward new one)
- **Anxieties:** What almost prevented adoption?
- **Competing solutions:** What are they comparing you to, including "do nothing"?
#### JTBD Output: The Job Story
Format better than "user story" for strategic decisions:
```
When [situation],
I want to [motivation/job],
So I can [expected outcome].
```
**Example (healthcare scheduling):**
```
When I'm trying to coordinate my parent's care from another city,
I want to see their upcoming appointments and have someone confirm changes,
So I can feel confident they won't miss critical treatments.
```
This is a different product than "schedule management software." The strategic implications — care coordination, family access, confirmation workflows — flow from the job.
#### JTBD → Product Strategy
| Job Insight | Strategic Implication |
|-------------|----------------------|
| Job is episodic (quarterly) | Engagement model must reach them before they need it |
| Job is habitual (daily) | DAU/MAU matters; build for habit formation |
| Job has high stakes | Trust and reliability > features; invest in onboarding + support |
| Job is social | Network effects possible; virality is structural, not a campaign |
| Job is delegated (done for someone else) | Two users: the buyer and the beneficiary. Design for both. |
---
### Category Design
If you're fighting for share in an existing category, you're playing defense on someone else's field.
**Category design premise:** Companies that define the category typically capture 76% of the market cap of that category. Name the category, own it.
#### The Category Design Process
**Step 1: Name the problem, not the solution.**
```
Wrong: "We make AI-powered customer support software."
Right: "The support team doesn't need more tickets. They need fewer problems."
```
**Step 2: Define the enemy.**
The enemy is the *old way* of solving the problem, not a competitor.
- Salesforce's enemy: spreadsheets and disconnected tools (not Siebel)
- Slack's enemy: email overload (not HipChat)
- Your enemy: ___________
**Step 3: Create the category name.**
It should be obvious in hindsight, not predictable in advance. Test it:
- Does it describe the problem, not the solution?
- Is it 2-3 words?
- Could a journalist use it without quoting you?
**Step 4: Missionary selling, not mercenary selling.**
Category kings educate the market before they sell to it. Content, thought leadership, community, and free tools all matter here — not as marketing tactics but as category creation.
**Step 5: Be the reference customer.**
Get the logos that define the category. The companies others look to. When others adopt, they don't want "a tool" — they want "what [Reference Customer] uses."
---
## 2. Competitive Moats
A moat is a structural advantage that compounds over time. Features are not moats. Pricing is not a moat. A moat is why, even if a competitor perfectly copies your product today, you still win.
### Moat Type 1: Network Effects
The product becomes more valuable as more users join. Two subtypes:
**Direct network effects:** Each user makes the product better for all other users (WhatsApp, Slack).
**Indirect network effects:** Each user on one side makes the product better for the other side (Uber drivers + riders, App Store developers + users).
**Data network effects:** More users → more data → better product → more users.
#### Network Effect Diagnostic
```
Question 1: Does adding user N make the product better for user N-1?
No → You don't have direct network effects
Yes → Map exactly how and how much
Question 2: Does adding user N make the product better for users on the OTHER side?
No → You don't have indirect network effects
Yes → Identify which side is the constraint (supply or demand)
Question 3: Does using the product generate data that improves the product?
No → You don't have data network effects
Yes → What is the data flywheel? Where does it compound?
```
**Building network effects intentionally:**
- Most products accidentally have weak network effects
- Design for network effects from Day 1: sharing, notifications, collaboration, integrations
- Measure network effect strength: "What % of new users were referred by existing users?"
### Moat Type 2: Switching Costs
The cost — time, money, risk — of leaving your product. The highest switching costs are:
| Switching Cost Type | Example | CPO Action |
|--------------------|---------|-----------|
| **Data lock-in** | Years of history, reports, trained models | Make data the experience, not just the storage |
| **Workflow integration** | 23 integrations, custom automations | Every integration is a switching cost. Build them. |
| **Team adoption** | Entire team trained on your tool | Multi-seat training investments pay switching cost dividends |
| **Contractual** | Annual contracts, SLAs | Long contracts are not a moat — customers resent them |
| **Process embedding** | Your product IS their process | Aim here. This is the deepest moat. |
**Warning:** Switching costs from data lock-in without value lock-in breed resentment, not loyalty. Customers who stay because they're trapped will leave the moment a migration tool appears.
### Moat Type 3: Data Advantages
Having data others can't easily get. Three subtypes:
**Proprietary data:** Data only you have access to (exclusive partnerships, sensor networks, unique user behavior at scale).
**Data scale:** Same type of data but at 10x the volume of competitors. Scale compounds model accuracy.
**Data variety:** Unique combination of data types. Not just usage data — usage + outcome data + external context.
**Testing your data moat:**
```
1. What data do we have that competitors don't?
2. At what volume does our data create a meaningfully better product?
3. Are we at that volume? If not, when?
4. Could a competitor buy or partner their way to equivalent data?
5. Is our data improving the product automatically, or only when we analyze it manually?
```
### Moat Type 4: Economies of Scale
Unit economics improve as you scale. Infrastructure costs drop per unit. Brand recognition lowers CAC. Negotiating power increases.
This is a real moat but the weakest one for product strategy — it doesn't keep faster-moving competitors from attacking while you're small.
### Moat Scorecard
Score each moat type 0-3 for your current product:
```
0 = Not present
1 = Weak / easily replicated
2 = Meaningful / takes 12-18 months to replicate
3 = Strong / structural advantage
Network effects (direct): __/3
Network effects (indirect): __/3
Network effects (data): __/3
Switching costs (data): __/3
Switching costs (workflow): __/3
Switching costs (team): __/3
Data advantages (exclusive): __/3
Data advantages (scale): __/3
Economies of scale: __/3
Total: __/27
< 9: No meaningful moat. Compete on execution speed.
9-15: Early moat. Identify and reinforce 1-2 strongest types.
16-21: Real moat. Invest to compound it.
> 21: Strong moat. Defend and expand.
```
---
## 3. Product Positioning
Positioning is not messaging. Positioning is the choice of: *Who is this for, what does it replace, and on what dimension do we win?*
### The Positioning Canvas (after April Dunford)
```
1. Competitive Alternatives
What would customers do if your product didn't exist?
(This is your real competition, not just your vendor category)
2. Unique Attributes
What capabilities do you have that alternatives lack?
(Features, but described neutrally, not as marketing)
3. Value (Outcomes)
What does each unique attribute enable for customers?
(Bridge from feature → outcome, not feature → feature)
4. Customer Who Cares
Who values those outcomes enough to pay for them?
(The customer segment for whom this value is highest)
5. Market Category
Where does the customer put you when comparing options?
(Frame the category to win, not to be fair)
6. Relevant Trends
What's changing in the world that makes this more valuable now?
(Why this moment? Urgency enabler.)
```
### Positioning Against Three Competitors
**Positioning vs. direct competitor:**
Identify one dimension where you structurally win. "Better" is not a position.
- Win on depth: more powerful in one scenario
- Win on simplicity: fewer decisions, fewer steps
- Win on integration: works with what they already use
- Win on price/value: same outcome, lower cost or risk
**Positioning vs. indirect alternative:**
The customer's current solution (spreadsheet, manual process, point solution).
- Make switching cost obvious (what are they giving up per week?)
- Make the switch simple (migration, onboarding, no data loss)
- Find the "aha moment" fast (value before they revert)
**Positioning vs. doing nothing:**
The hardest competitor. Status quo has zero switching cost.
- Quantify the cost of inaction (time, risk, revenue, competitive risk)
- Find the trigger event that makes inaction intolerable
- Show the risk is higher than the switch cost
### Positioning Failure Modes
| Failure | Description | Fix |
|---------|-------------|-----|
| **For everyone** | No segment. "Any company that needs X." | Name the best-fit customer. |
| **Feature positioning** | "The only tool with [feature X]" | Features are table stakes. Lead with outcome. |
| **Vague differentiation** | "Easier, faster, better" | Measurable, specific, or don't say it. |
| **Category misfit** | In a category where you can't win | Either own the category or name a new one |
| **Lagging positioning** | Positioned for who you were, not who you are | Reposition every 18-24 months or after major product change |
---
## 4. Portfolio Management
### Applying BCG Matrix to Product Lines
BCG matrix was designed for business units. Applied to product lines:
**Inputs:**
- Market growth rate (industry growth, not your growth)
- Relative market share (your share vs. largest competitor)
- Revenue contribution (absolute)
- Investment level (engineering + sales + marketing per product)
**Calculation:**
```
Market share ratio = Your market share / Largest competitor's market share
Growth rate = Market CAGR (next 3 years estimate)
Stars: share ratio > 1.0, growth > 10%
Cash Cows: share ratio > 1.0, growth < 10%
Question Marks: share ratio < 1.0, growth > 10%
Dogs: share ratio < 1.0, growth < 10%
```
### Portfolio Allocation Rules
**Star products:**
- Invest at or above market growth rate
- Goal: maintain share leadership as market grows
- Don't extract cash — reinvest
- Metrics: market share trend, NPS, retention, feature velocity
**Cash Cow products:**
- Minimum investment to maintain market position
- Goal: maximize free cash flow
- Resist the urge to innovate — incremental improvements only
- Metrics: gross margin, churn rate, support cost per customer
**Question Mark products:**
- Binary decision: invest to win or exit
- "Maintain" is not a strategy for question marks — you lose share every quarter you're neutral
- Set a deadline (2 quarters) and a threshold for investment decision
- Metrics: share gain rate, customer acquisition efficiency
**Dog products:**
- Decision: sell, sunset, or bundle
- Never "fix" a dog with more investment
- Timeline to sunset: 6-12 months, migration plan for existing customers
- Metrics: customer migration rate, revenue retained
### Portfolio Review Template
Run quarterly. One slide per product.
```
Product: [Name]
Current Quadrant: [Star/Cash Cow/Question Mark/Dog]
Revenue this quarter: $___
Revenue growth QoQ: ___%
Market share estimate: ___%
Investment level (% of eng capacity): ___%
Investment posture: [Invest / Maintain / Kill]
Key metric: [Name] → [Current value] → [QoQ trend]
Top risk: [One thing that could change this assessment]
Decision required: [Yes/No] | [What decision?]
```
### The Honest Portfolio Conversation
Questions CPOs avoid but boards ask:
- "Which product would we kill if we had to? What's stopping us?"
- "Are we funding dogs because the team is attached or because there's a real plan?"
- "What would our margins look like if we stopped investing in the bottom 2 products?"
- "What's the dependency between our products? Are we a platform or a bundle of unrelated tools?"
---
## 5. Board-Level Product Reporting
### What Good Looks Like
Board product updates fail in three ways:
1. Too much roadmap detail (feature list masquerading as strategy)
2. No trend context (showing a number without showing if it's getting better or worse)
3. No risks (all good news = no credibility)
### The 5-Slide Board Product Update
**Slide 1: North Star Metric**
```
Title: Product Health — [Quarter]
[Chart: North star metric over last 12 months, quarterly cohorts]
This quarter: [Value] | Prior quarter: [Value] | YoY: [Value]
Target: [Value] | Status: On track / At risk / Behind
Drivers (2-3 bullets):
• What's driving improvement: ___
• What's dragging: ___
• What we're doing about the drag: ___
```
**Slide 2: Retention and PMF**
```
Title: Product-Market Fit Evidence
[Chart: D30 retention by cohort, last 6 cohorts]
[Callout: Sean Ellis score = XX% (target: > 40%)]
PMF status: Achieved / Approaching / Not yet
Best segment: [Describe — where retention is strongest]
Weakest segment: [Describe — and what we're doing about it]
```
**Slide 3: Portfolio Status**
```
Title: Portfolio — Invest / Maintain / Kill
| Product | Quadrant | Revenue | Growth | Posture | Risk |
|---------|---------|---------|--------|---------|------|
| [A] | Star | $___ | +XX% | Invest | ___ |
| [B] | Cash Cow| $___ | +X% | Maintain| ___ |
| [C] | Dog | $___ | -X% | Kill Q3 | ___ |
Changes since last quarter: ___
Decisions needed from board: ___
```
**Slide 4: Strategic Bets**
```
Title: Bets This Half — [H1/H2]
Bet 1: [Name]
Hypothesis: If we [do X], [segment Y] will [do Z]
Evidence so far: [Data]
Confidence: [Low / Medium / High]
Decision point: [When do we know?] [What will we measure?]
Bet 2: [Name]
[Same structure]
```
**Slide 5: Top Risks**
```
Title: Product Risks — [Quarter]
Risk 1: [Name]
What it is: ___
Probability: [Low/Med/High]
Impact if realized: ___
Mitigation: ___
Risk 2: [Name]
[Same structure]
Risk 3: [Name]
[Same structure]
```
### Delivering in the Board Meeting
- Never read the slide
- Lead with the conclusion, not the data
- Prepare for "what if that assumption is wrong?" for every bet
- When something underperformed: say it, own it, explain what changed
- Never present a number you can't explain 3 levels deep
**Example of bad delivery:**
"Our north star is up 15% QoQ, which is great. We're tracking well."
**Example of good delivery:**
"North star is up 15% — ahead of plan. The majority of that is from the enterprise cohort activated in October, driven by the workflow automation feature we shipped in September. The consumer segment is flat, which is a concern. We're running three experiments this quarter to diagnose whether that's an acquisition problem or an activation problem — I'll have an answer for next quarter."
---
## Quick Reference: Framework Summary
| Need | Framework |
|------|----------|
| Why do customers use us? | Jobs to Be Done |
| How do we define our market? | Category Design |
| What's our structural advantage? | Moat Scorecard |
| How do we position? | April Dunford Positioning Canvas |
| Which products to fund? | BCG Matrix + Invest/Maintain/Kill |
| How to report to the board? | 5-Slide Board Update |
FILE:cpo-advisor/scripts/pmf_scorer.py
#!/usr/bin/env python3
"""
PMF Scorer — Multi-dimensional Product-Market Fit analysis.
Scores PMF across four dimensions:
- Retention (40%): D30 and D90 cohort retention
- Engagement (25%): DAU/MAU, session depth, key action rate
- Satisfaction(20%): Sean Ellis score, NPS
- Growth (15%): Organic signup rate, referral rate
Usage:
python pmf_scorer.py # Run with built-in sample data
python pmf_scorer.py --input data.json # Run with your data
JSON input format: see sample_data() function below.
"""
import json
import sys
import argparse
import math
from typing import Optional
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
def sample_data() -> dict:
"""
Sample input data. Replace with your own values.
All fields are optional — missing fields score 0 for that sub-metric
and a note is added to recommendations.
"""
return {
"product_name": "Acme SaaS",
"business_model": "b2b_saas", # b2b_saas | consumer | marketplace | plg
# Retention: D30 and D90 as decimals (e.g. 0.42 = 42%)
# Provide multiple cohorts if available. Most recent first.
"retention": {
"d30_cohorts": [0.38, 0.41, 0.44, 0.43], # newest → oldest
"d90_cohorts": [0.28, 0.30, 0.31],
"curve_flattening": True, # Does the curve flatten (vs. continuing to drop)?
},
# Engagement
"engagement": {
"dau_mau_ratio": 0.24, # Daily active / Monthly active (decimal)
"avg_sessions_per_week": 3.2, # Per active user
"key_action_rate": 0.55, # % of users who performed core value action in last 30d
"session_depth_score": 0.6, # 0-1: 0 = one page, 1 = full feature exploration
},
# Satisfaction
"satisfaction": {
"sean_ellis_very_disappointed": 0.38, # Fraction (e.g. 0.38 = 38%)
"sean_ellis_sample_size": 87, # Raw response count
"nps_score": 34, # -100 to 100
"nps_sample_size": 210,
},
# Growth
"growth": {
"organic_signup_pct": 0.27, # % of new signups from organic/referral/WOM
"referral_rate": 0.18, # % of active users who referred someone last 90d
"mom_growth_rate": 0.08, # Month-over-month new user growth (decimal)
},
}
# ---------------------------------------------------------------------------
# Thresholds by business model
# ---------------------------------------------------------------------------
THRESHOLDS = {
"b2b_saas": {
"d30_pmf": 0.40, "d30_strong": 0.60,
"d90_pmf": 0.25, "d90_strong": 0.45,
"dau_mau_pmf": 0.15, "dau_mau_strong": 0.35,
"sean_ellis_pmf": 0.40, "sean_ellis_strong": 0.55,
"nps_pmf": 30, "nps_strong": 50,
},
"consumer": {
"d30_pmf": 0.20, "d30_strong": 0.35,
"d90_pmf": 0.10, "d90_strong": 0.20,
"dau_mau_pmf": 0.20, "dau_mau_strong": 0.40,
"sean_ellis_pmf": 0.40, "sean_ellis_strong": 0.55,
"nps_pmf": 20, "nps_strong": 45,
},
"marketplace": {
"d30_pmf": 0.30, "d30_strong": 0.50,
"d90_pmf": 0.20, "d90_strong": 0.35,
"dau_mau_pmf": 0.15, "dau_mau_strong": 0.30,
"sean_ellis_pmf": 0.40, "sean_ellis_strong": 0.55,
"nps_pmf": 25, "nps_strong": 45,
},
"plg": {
"d30_pmf": 0.25, "d30_strong": 0.45,
"d90_pmf": 0.15, "d90_strong": 0.30,
"dau_mau_pmf": 0.20, "dau_mau_strong": 0.40,
"sean_ellis_pmf": 0.40, "sean_ellis_strong": 0.55,
"nps_pmf": 30, "nps_strong": 50,
},
}
# Weights for the four dimensions (must sum to 1.0)
DIMENSION_WEIGHTS = {
"retention": 0.40,
"engagement": 0.25,
"satisfaction": 0.20,
"growth": 0.15,
}
# ---------------------------------------------------------------------------
# Scoring helpers
# ---------------------------------------------------------------------------
def clamp(value: float, lo: float = 0.0, hi: float = 1.0) -> float:
return max(lo, min(hi, value))
def score_between(value: Optional[float], lo: float, hi: float) -> float:
"""Linear interpolation: lo → 0.0, hi → 1.0, beyond hi → 1.0."""
if value is None:
return 0.0
if value <= lo:
return 0.0
if value >= hi:
return 1.0
return (value - lo) / (hi - lo)
def cohort_trend(cohorts: list) -> float:
"""
Given cohorts newest-first, return a trend score -1 to +1.
Positive = improving. Negative = degrading.
"""
if len(cohorts) < 2:
return 0.0
# Simple: compare most recent half average vs. older half average
mid = len(cohorts) // 2
recent_avg = sum(cohorts[:mid]) / mid if mid else cohorts[0]
older_avg = sum(cohorts[mid:]) / (len(cohorts) - mid)
if older_avg == 0:
return 0.0
delta = (recent_avg - older_avg) / older_avg
return clamp(delta * 5, -1.0, 1.0) # scale: 20% improvement = score of 1.0
# ---------------------------------------------------------------------------
# Dimension scorers
# ---------------------------------------------------------------------------
def score_retention(data: dict, thresholds: dict) -> tuple[float, list]:
"""Returns (score 0-1, list of findings)."""
r = data.get("retention", {})
findings = []
scores = []
d30 = r.get("d30_cohorts", [])
d90 = r.get("d90_cohorts", [])
if not d30:
findings.append("⚠ No D30 retention data — this is the most important PMF signal. Instrument it immediately.")
return 0.0, findings
latest_d30 = d30[0]
d30_score = score_between(latest_d30, 0, thresholds["d30_strong"])
scores.append(d30_score)
if latest_d30 >= thresholds["d30_strong"]:
findings.append(f"✓ D30 retention {latest_d30:.0%} — strong PMF signal")
elif latest_d30 >= thresholds["d30_pmf"]:
findings.append(f"◑ D30 retention {latest_d30:.0%} — approaching PMF threshold ({thresholds['d30_pmf']:.0%})")
else:
findings.append(f"✗ D30 retention {latest_d30:.0%} — below PMF threshold ({thresholds['d30_pmf']:.0%}). Focus here before anything else.")
# Trend bonus
if len(d30) >= 2:
trend = cohort_trend(d30)
trend_score = (trend + 1) / 2 # normalize to 0-1
scores.append(trend_score * 0.5) # trend is bonus, not primary
if trend > 0.1:
findings.append(f"✓ D30 retention improving across cohorts — strong learning signal")
elif trend < -0.1:
findings.append(f"✗ D30 retention declining across cohorts — product changes may be hurting core users")
if d90:
latest_d90 = d90[0]
d90_score = score_between(latest_d90, 0, thresholds["d90_strong"])
scores.append(d90_score)
if latest_d90 >= thresholds["d90_strong"]:
findings.append(f"✓ D90 retention {latest_d90:.0%} — excellent long-term retention")
elif latest_d90 >= thresholds["d90_pmf"]:
findings.append(f"◑ D90 retention {latest_d90:.0%} — some long-term value demonstrated")
else:
findings.append(f"✗ D90 retention {latest_d90:.0%} — users not finding long-term value")
else:
findings.append("⚠ No D90 data. Add 90-day cohort tracking.")
flattening = r.get("curve_flattening", False)
if flattening:
scores.append(0.8)
findings.append("✓ Retention curve flattening — core retained segment exists")
else:
scores.append(0.2)
findings.append("✗ Retention curve not flattening — no stable retained segment yet")
return clamp(sum(scores) / len(scores)), findings
def score_engagement(data: dict, thresholds: dict) -> tuple[float, list]:
e = data.get("engagement", {})
findings = []
scores = []
dau_mau = e.get("dau_mau_ratio")
if dau_mau is not None:
s = score_between(dau_mau, 0, thresholds["dau_mau_strong"])
scores.append(s)
if dau_mau >= thresholds["dau_mau_strong"]:
findings.append(f"✓ DAU/MAU {dau_mau:.0%} — strong daily habit")
elif dau_mau >= thresholds["dau_mau_pmf"]:
findings.append(f"◑ DAU/MAU {dau_mau:.0%} — moderate engagement")
else:
findings.append(f"✗ DAU/MAU {dau_mau:.0%} — users not building a habit. Find the daily job or accept weekly use pattern.")
else:
findings.append("⚠ No DAU/MAU data.")
sessions = e.get("avg_sessions_per_week")
if sessions is not None:
# 5+ sessions/week = strong, 2 = threshold
s = score_between(sessions, 1, 5)
scores.append(s)
if sessions >= 5:
findings.append(f"✓ {sessions:.1f} sessions/week — high engagement")
elif sessions >= 2:
findings.append(f"◑ {sessions:.1f} sessions/week — moderate")
else:
findings.append(f"✗ {sessions:.1f} sessions/week — very low. Users not returning within week.")
else:
findings.append("⚠ No session frequency data.")
kar = e.get("key_action_rate")
if kar is not None:
s = score_between(kar, 0.10, 0.70)
scores.append(s)
if kar >= 0.60:
findings.append(f"✓ Key action rate {kar:.0%} — core value well-adopted")
elif kar >= 0.30:
findings.append(f"◑ Key action rate {kar:.0%} — improve onboarding to drive this up")
else:
findings.append(f"✗ Key action rate {kar:.0%} — most users not reaching core value. This is an activation problem.")
else:
findings.append("⚠ No key action rate. Define your 'aha moment' action and track it.")
depth = e.get("session_depth_score")
if depth is not None:
scores.append(depth)
if depth >= 0.6:
findings.append(f"✓ Session depth {depth:.1f} — users exploring the product")
else:
findings.append(f"◑ Session depth {depth:.1f} — users sticking to narrow feature set")
if not scores:
return 0.0, findings
return clamp(sum(scores) / len(scores)), findings
def score_satisfaction(data: dict, thresholds: dict) -> tuple[float, list]:
s_data = data.get("satisfaction", {})
findings = []
scores = []
se_score = s_data.get("sean_ellis_very_disappointed")
se_n = s_data.get("sean_ellis_sample_size", 0)
if se_score is not None:
if se_n < 40:
findings.append(f"⚠ Sean Ellis n={se_n} — too small to be reliable. Need 40+ responses.")
scores.append(score_between(se_score, 0, thresholds["sean_ellis_strong"]) * 0.5) # half weight
else:
s = score_between(se_score, 0, thresholds["sean_ellis_strong"])
scores.append(s)
if se_score >= thresholds["sean_ellis_strong"]:
findings.append(f"✓ Sean Ellis {se_score:.0%} 'very disappointed' — strong PMF signal (n={se_n})")
elif se_score >= thresholds["sean_ellis_pmf"]:
findings.append(f"◑ Sean Ellis {se_score:.0%} — at PMF threshold. Push to > {thresholds['sean_ellis_strong']:.0%}.")
else:
findings.append(f"✗ Sean Ellis {se_score:.0%} — below {thresholds['sean_ellis_pmf']:.0%} threshold. Interview 'somewhat disappointed' group.")
else:
findings.append("⚠ No Sean Ellis data. Run a one-question survey to your active users now.")
nps = s_data.get("nps_score")
nps_n = s_data.get("nps_sample_size", 0)
if nps is not None:
if nps_n < 50:
findings.append(f"⚠ NPS n={nps_n} — sample too small. Need 50+ for reliability.")
# NPS ranges from -100 to 100; normalize to 0-1 against threshold
s = score_between(nps, -20, thresholds["nps_strong"])
scores.append(s)
if nps >= thresholds["nps_strong"]:
findings.append(f"✓ NPS {nps} — excellent. Promoters will drive organic growth.")
elif nps >= thresholds["nps_pmf"]:
findings.append(f"◑ NPS {nps} — acceptable. Focus on converting passives to promoters.")
elif nps >= 0:
findings.append(f"✗ NPS {nps} — low. More detractors than promoters is a warning sign.")
else:
findings.append(f"✗ NPS {nps} — negative. Active detractors outnumber promoters.")
else:
findings.append("⚠ No NPS data.")
if not scores:
return 0.0, findings
return clamp(sum(scores) / len(scores)), findings
def score_growth(data: dict, _thresholds: dict) -> tuple[float, list]:
g = data.get("growth", {})
findings = []
scores = []
organic_pct = g.get("organic_signup_pct")
if organic_pct is not None:
s = score_between(organic_pct, 0.05, 0.50)
scores.append(s)
if organic_pct >= 0.30:
findings.append(f"✓ {organic_pct:.0%} organic signups — word of mouth is working")
elif organic_pct >= 0.20:
findings.append(f"◑ {organic_pct:.0%} organic — moderate. Build referral loop deliberately.")
else:
findings.append(f"✗ {organic_pct:.0%} organic — almost all paid. PMF may not be strong enough to generate word of mouth.")
else:
findings.append("⚠ No organic signup tracking. Tag all signup sources now.")
referral = g.get("referral_rate")
if referral is not None:
s = score_between(referral, 0.05, 0.35)
scores.append(s)
if referral >= 0.25:
findings.append(f"✓ {referral:.0%} of active users referring — strong viral signal")
elif referral >= 0.15:
findings.append(f"◑ {referral:.0%} referral rate — building. Add referral incentive or friction removal.")
else:
findings.append(f"✗ {referral:.0%} referral rate — users not recommending. Satisfaction or network effects missing.")
else:
findings.append("⚠ No referral rate data.")
mom = g.get("mom_growth_rate")
if mom is not None:
s = score_between(mom, 0, 0.20)
scores.append(s)
if mom >= 0.15:
findings.append(f"✓ {mom:.0%} MoM growth — strong momentum")
elif mom >= 0.08:
findings.append(f"◑ {mom:.0%} MoM growth — moderate. Identify top acquisition channel and double it.")
else:
findings.append(f"✗ {mom:.0%} MoM growth — slow. Acquisition is a bottleneck.")
if not scores:
return 0.0, findings
return clamp(sum(scores) / len(scores)), findings
# ---------------------------------------------------------------------------
# Overall scoring and recommendations
# ---------------------------------------------------------------------------
def pmf_status(overall: float) -> tuple[str, str]:
"""Returns (status label, description)."""
if overall >= 0.80:
return "STRONG PMF", "Clear product-market fit. Shift focus to scaling acquisition and defending moat."
elif overall >= 0.60:
return "PMF APPROACHING", "Meaningful signals present. Identify and remove the 1-2 friction points blocking retention."
elif overall >= 0.40:
return "EARLY SIGNALS", "Weak PMF. Some users find value. Narrow your ICP and double down on what's working."
elif overall >= 0.20:
return "PRE-PMF", "No clear PMF yet. Don't scale acquisition. Focus entirely on retention experiments."
else:
return "NO SIGNAL", "No PMF signals detected. Revisit the problem hypothesis before investing further in the solution."
def top_recommendations(dim_scores: dict, data: dict) -> list[str]:
"""Prioritized recommendations based on weakest dimensions."""
recs = []
model = data.get("business_model", "b2b_saas")
ranked = sorted(dim_scores.items(), key=lambda x: x[1])
for dim, score in ranked:
if score < 0.40:
if dim == "retention":
recs.append(
"CRITICAL — Retention: Run cohort analysis by segment. Find the cohort with highest D30. "
"Interview 10 of those users. Build for them exclusively until retention flattens."
)
elif dim == "engagement":
recs.append(
"Engagement: Define your 'aha moment' — the one action that predicts long-term retention. "
"Measure time-to-aha. Remove every friction point on that path."
)
elif dim == "satisfaction":
recs.append(
"Satisfaction: Run Sean Ellis survey immediately (need n ≥ 40). "
"Interview every 'somewhat disappointed' user — the gap between 'somewhat' and 'very' is your product gap."
)
elif dim == "growth":
recs.append(
"Growth: Track signup source for every new user. If organic < 20%, "
"you may be papering over weak PMF with paid acquisition. Fix retention first."
)
if not recs:
recs.append(
"All dimensions scoring above threshold. Focus: "
"(1) Defend moat, (2) Expand ICP carefully, (3) Build referral flywheel."
)
if model == "b2b_saas":
recs.append("B2B tip: Track NRR (Net Revenue Retention). PMF in B2B requires expansion, not just retention.")
elif model == "consumer":
recs.append("Consumer tip: Find your D7 'magic moment'. The habit window is small — optimize for it.")
elif model == "plg":
recs.append("PLG tip: Define your PQL (product-qualified lead). The activation event that predicts paid conversion.")
elif model == "marketplace":
recs.append("Marketplace tip: Measure both sides separately. PMF on demand side ≠ PMF on supply side.")
return recs
# ---------------------------------------------------------------------------
# Report renderer
# ---------------------------------------------------------------------------
def render_report(data: dict, dim_scores: dict, dim_findings: dict, overall: float) -> str:
status, description = pmf_status(overall)
recs = top_recommendations(dim_scores, data)
lines = []
lines.append("=" * 60)
lines.append(f" PMF SCORER — {data.get('product_name', 'Product')}")
lines.append(f" Model: {data.get('business_model', 'unknown').upper()}")
lines.append("=" * 60)
lines.append("")
# Overall
bar_len = 40
filled = round(overall * bar_len)
bar = "█" * filled + "░" * (bar_len - filled)
lines.append(f" Overall PMF Score: {overall:.0%}")
lines.append(f" [{bar}]")
lines.append(f" Status: {status}")
lines.append(f" {description}")
lines.append("")
# Dimension breakdown
lines.append(" DIMENSION SCORES")
lines.append(" " + "-" * 50)
for dim, weight in DIMENSION_WEIGHTS.items():
score = dim_scores.get(dim, 0.0)
dim_bar_len = 20
dim_filled = round(score * dim_bar_len)
dim_bar = "█" * dim_filled + "░" * (dim_bar_len - dim_filled)
label = dim.capitalize().ljust(12)
lines.append(f" {label} [{dim_bar}] {score:.0%} (weight: {weight:.0%})")
lines.append("")
# Findings per dimension
for dim in ["retention", "engagement", "satisfaction", "growth"]:
findings = dim_findings.get(dim, [])
if findings:
lines.append(f" {dim.upper()} FINDINGS")
for f in findings:
lines.append(f" {f}")
lines.append("")
# Recommendations
lines.append(" PRIORITIZED RECOMMENDATIONS")
lines.append(" " + "-" * 50)
for i, rec in enumerate(recs, 1):
# Wrap at 70 chars
words = rec.split()
line = f" {i}. "
for word in words:
if len(line) + len(word) + 1 > 72:
lines.append(line)
line = " " + word + " "
else:
line += word + " "
lines.append(line.rstrip())
lines.append("")
lines.append("=" * 60)
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def run(data: dict) -> dict:
"""
Score PMF from input data dict.
Returns dict with overall score, dimension scores, and findings.
"""
model = data.get("business_model", "b2b_saas")
thresholds = THRESHOLDS.get(model, THRESHOLDS["b2b_saas"])
dim_scores = {}
dim_findings = {}
ret_score, ret_findings = score_retention(data, thresholds)
dim_scores["retention"] = ret_score
dim_findings["retention"] = ret_findings
eng_score, eng_findings = score_engagement(data, thresholds)
dim_scores["engagement"] = eng_score
dim_findings["engagement"] = eng_findings
sat_score, sat_findings = score_satisfaction(data, thresholds)
dim_scores["satisfaction"] = sat_score
dim_findings["satisfaction"] = sat_findings
grow_score, grow_findings = score_growth(data, thresholds)
dim_scores["growth"] = grow_score
dim_findings["growth"] = grow_findings
overall = sum(
dim_scores[dim] * weight
for dim, weight in DIMENSION_WEIGHTS.items()
)
return {
"overall": overall,
"dim_scores": dim_scores,
"dim_findings": dim_findings,
"status": pmf_status(overall)[0],
}
def main():
parser = argparse.ArgumentParser(
description="PMF Scorer — Multi-dimensional Product-Market Fit analysis",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--input", "-i",
metavar="FILE",
help="JSON file with your product data (default: built-in sample data)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output raw JSON instead of formatted report",
)
args = parser.parse_args()
if args.input:
try:
with open(args.input) as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: file not found: {args.input}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
else:
print("No input file provided — running with sample data.\n")
data = sample_data()
result = run(data)
if args.json:
output = {
"product_name": data.get("product_name"),
"business_model": data.get("business_model"),
"overall_score": round(result["overall"], 4),
"overall_pct": f"{result['overall']:.0%}",
"status": result["status"],
"dimensions": {
dim: {
"score": round(result["dim_scores"][dim], 4),
"pct": f"{result['dim_scores'][dim]:.0%}",
"weight": f"{DIMENSION_WEIGHTS[dim]:.0%}",
"findings": result["dim_findings"][dim],
}
for dim in DIMENSION_WEIGHTS
},
}
print(json.dumps(output, indent=2))
else:
print(render_report(data, result["dim_scores"], result["dim_findings"], result["overall"]))
if __name__ == "__main__":
main()
FILE:cpo-advisor/scripts/portfolio_analyzer.py
#!/usr/bin/env python3
"""
Portfolio Analyzer — Product portfolio BCG matrix classification and investment analysis.
For each product, classifies into BCG quadrant (Star, Cash Cow, Question Mark, Dog)
and generates investment recommendations (Invest / Maintain / Kill).
Usage:
python portfolio_analyzer.py # Run with built-in sample data
python portfolio_analyzer.py --input data.json # Run with your data
python portfolio_analyzer.py --json # Output raw JSON
JSON input format: see sample_data() function below.
"""
import json
import sys
import argparse
from typing import Optional
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
def sample_data() -> dict:
"""
Sample portfolio. Replace with real product data.
Fields:
name Product name
revenue_quarterly Current quarter revenue (any consistent currency)
revenue_prev_q Revenue last quarter (for QoQ calculation)
market_growth_pct Annual market growth rate (percent, e.g. 12.5 for 12.5%)
your_market_share Your estimated market share (percent, e.g. 8.0 for 8%)
largest_competitor_share Largest competitor's share (percent)
eng_capacity_pct % of total engineering capacity allocated (0-100)
d30_retention Optional D30 retention rate (decimal, e.g. 0.45)
nps Optional NPS score (-100 to 100)
notes Optional free text notes for the report
"""
return {
"company": "Acme Corp",
"total_engineering_headcount": 45,
"products": [
{
"name": "CorePlatform",
"revenue_quarterly": 480000,
"revenue_prev_q": 430000,
"market_growth_pct": 22.0,
"your_market_share": 18.0,
"largest_competitor_share": 12.0,
"eng_capacity_pct": 35,
"d30_retention": 0.61,
"nps": 52,
"notes": "Our flagship. Leading market share in fast-growing segment.",
},
{
"name": "ReportingModule",
"revenue_quarterly": 290000,
"revenue_prev_q": 285000,
"market_growth_pct": 5.0,
"your_market_share": 22.0,
"largest_competitor_share": 18.0,
"eng_capacity_pct": 25,
"d30_retention": 0.58,
"nps": 38,
"notes": "Mature product, strong margins, slow market.",
},
{
"name": "MobileApp",
"revenue_quarterly": 95000,
"revenue_prev_q": 78000,
"market_growth_pct": 35.0,
"your_market_share": 3.5,
"largest_competitor_share": 24.0,
"eng_capacity_pct": 28,
"d30_retention": 0.31,
"nps": 22,
"notes": "High growth market. We're far behind on share. Bet or exit.",
},
{
"name": "LegacyConnector",
"revenue_quarterly": 62000,
"revenue_prev_q": 68000,
"market_growth_pct": -3.0,
"your_market_share": 8.0,
"largest_competitor_share": 35.0,
"eng_capacity_pct": 12,
"d30_retention": 0.42,
"nps": 14,
"notes": "Declining market. Customers are on long-term contracts.",
},
],
}
# ---------------------------------------------------------------------------
# BCG Classification
# ---------------------------------------------------------------------------
# Growth rate threshold: markets growing faster than this are "high growth"
GROWTH_THRESHOLD_PCT = 10.0
# Market share ratio threshold: ratio > 1.0 means you lead the market
SHARE_RATIO_THRESHOLD = 1.0
def bcg_quadrant(market_growth_pct: float, share_ratio: float) -> str:
high_growth = market_growth_pct >= GROWTH_THRESHOLD_PCT
leading_share = share_ratio >= SHARE_RATIO_THRESHOLD
if high_growth and leading_share:
return "Star"
elif not high_growth and leading_share:
return "Cash Cow"
elif high_growth and not leading_share:
return "Question Mark"
else:
return "Dog"
def quadrant_emoji(quadrant: str) -> str:
return {
"Star": "⭐",
"Cash Cow": "🐄",
"Question Mark": "❓",
"Dog": "🐕",
}.get(quadrant, "?")
def investment_posture(quadrant: str, qoq_growth: float, retention: Optional[float]) -> str:
"""
Invest / Maintain / Kill recommendation with nuance.
"""
if quadrant == "Star":
return "Invest"
elif quadrant == "Cash Cow":
# If cash cow is declining fast or retention is poor, consider killing
if qoq_growth < -0.10 or (retention is not None and retention < 0.30):
return "Kill"
return "Maintain"
elif quadrant == "Question Mark":
# Fast QoQ growth signals the bet might pay off → Invest
# Flat or slow QoQ with weak retention → Kill
if qoq_growth >= 0.15 and (retention is None or retention >= 0.25):
return "Invest"
elif qoq_growth < 0.05 or (retention is not None and retention < 0.20):
return "Kill"
return "Evaluate" # Needs explicit strategic decision
else: # Dog
if qoq_growth > 0.10 and (retention is None or retention >= 0.35):
return "Evaluate" # Surprising momentum — verify before killing
return "Kill"
def posture_color(posture: str) -> str:
return {
"Invest": "✓",
"Maintain": "◑",
"Kill": "✗",
"Evaluate": "⚠",
}.get(posture, "?")
# ---------------------------------------------------------------------------
# Product analysis
# ---------------------------------------------------------------------------
def analyze_product(p: dict) -> dict:
revenue_q = p.get("revenue_quarterly", 0)
revenue_prev = p.get("revenue_prev_q", revenue_q)
qoq_growth = (revenue_q - revenue_prev) / revenue_prev if revenue_prev else 0.0
your_share = p.get("your_market_share", 0)
competitor_share = p.get("largest_competitor_share", 1)
share_ratio = your_share / competitor_share if competitor_share else 0.0
market_growth = p.get("market_growth_pct", 0)
retention = p.get("d30_retention")
nps = p.get("nps")
eng_pct = p.get("eng_capacity_pct", 0)
quadrant = bcg_quadrant(market_growth, share_ratio)
posture = investment_posture(quadrant, qoq_growth, retention)
# Alignment score: how well does engineering investment match the recommended posture?
# Invest products should have high eng allocation; Kill products should have low.
alignment_score = _compute_alignment(posture, eng_pct)
return {
"name": p.get("name", "Unknown"),
"revenue_quarterly": revenue_q,
"revenue_prev_q": revenue_prev,
"qoq_growth": qoq_growth,
"market_growth_pct": market_growth,
"your_market_share": your_share,
"largest_competitor_share": competitor_share,
"share_ratio": share_ratio,
"eng_capacity_pct": eng_pct,
"d30_retention": retention,
"nps": nps,
"quadrant": quadrant,
"posture": posture,
"alignment_score": alignment_score,
"notes": p.get("notes", ""),
"findings": _product_findings(quadrant, posture, qoq_growth, share_ratio,
market_growth, retention, nps, eng_pct),
}
def _compute_alignment(posture: str, eng_pct: float) -> float:
"""
Returns 0.0-1.0 score. High = engineering allocation matches strategic posture.
"""
targets = {"Invest": 0.35, "Maintain": 0.15, "Kill": 0.05, "Evaluate": 0.20}
target = targets.get(posture, 0.20)
deviation = abs(eng_pct / 100 - target)
return max(0.0, 1.0 - (deviation / 0.35))
def _product_findings(
quadrant: str, posture: str,
qoq_growth: float, share_ratio: float, market_growth: float,
retention: Optional[float], nps: Optional[int], eng_pct: float
) -> list:
findings = []
if quadrant == "Star":
if eng_pct < 30:
findings.append(f"⚠ Star product getting only {eng_pct}% of eng capacity — likely underinvested. Stars need fuel.")
else:
findings.append(f"✓ Star product with {eng_pct}% eng allocation — appropriate investment.")
if share_ratio < 1.5:
findings.append(f"◑ Share ratio {share_ratio:.1f}x — leading but not dominant. Accelerate to widen the gap.")
else:
findings.append(f"✓ Share ratio {share_ratio:.1f}x — strong lead. Defend aggressively.")
elif quadrant == "Cash Cow":
if eng_pct > 25:
findings.append(f"⚠ Cash Cow getting {eng_pct}% of eng — overinvested. Reduce to 10-15% max. Redeploy to Stars.")
else:
findings.append(f"✓ Cash Cow with {eng_pct}% eng — appropriate. Don't innovate, just maintain.")
if qoq_growth < -0.05:
findings.append(f"⚠ Revenue declining {abs(qoq_growth):.0%} QoQ — monitor for transition to Dog.")
else:
findings.append(f"✓ Revenue stable (QoQ: {qoq_growth:+.0%}) — milk this.")
elif quadrant == "Question Mark":
findings.append(f"⚠ Fast market ({market_growth:.0f}% growth) but only {share_ratio:.1f}x relative share.")
findings.append(f" Decision required: Invest to capture share or exit. 'Maintain' loses share every quarter.")
if qoq_growth >= 0.15:
findings.append(f"✓ QoQ growth {qoq_growth:+.0%} — momentum building. Investment may be justified.")
elif qoq_growth < 0.05:
findings.append(f"✗ QoQ growth {qoq_growth:+.0%} — stalled despite hot market. Strong exit signal.")
elif quadrant == "Dog":
findings.append(f"✗ Low share ({share_ratio:.1f}x) in slow/declining market ({market_growth:.0f}% growth).")
if eng_pct > 10:
findings.append(f"✗ Dog consuming {eng_pct}% of eng capacity. Set a sunset date. Migrate customers.")
if qoq_growth > 0:
findings.append(f"◑ Slight QoQ growth ({qoq_growth:+.0%}) — verify whether this is genuine or contract timing.")
if retention is not None:
if retention < 0.30:
findings.append(f"✗ D30 retention {retention:.0%} — users not finding value. Weak unit economics for any posture.")
elif retention >= 0.50:
findings.append(f"✓ D30 retention {retention:.0%} — users find value. Supports investment or stable maintenance.")
if nps is not None:
if nps < 0:
findings.append(f"✗ NPS {nps} — net detractors. Word of mouth is negative. Fix before scaling.")
elif nps >= 40:
findings.append(f"✓ NPS {nps} — strong promoter base. Harness for referrals.")
return findings
# ---------------------------------------------------------------------------
# Portfolio-level analysis
# ---------------------------------------------------------------------------
def analyze_portfolio(data: dict) -> dict:
products = [analyze_product(p) for p in data.get("products", [])]
total_revenue = sum(p["revenue_quarterly"] for p in products)
total_eng = sum(p["eng_capacity_pct"] for p in products)
# Revenue by quadrant
quadrant_revenue = {}
quadrant_eng = {}
for p in products:
q = p["quadrant"]
quadrant_revenue[q] = quadrant_revenue.get(q, 0) + p["revenue_quarterly"]
quadrant_eng[q] = quadrant_eng.get(q, 0) + p["eng_capacity_pct"]
# Portfolio health score
health = _portfolio_health(products, total_revenue, total_eng)
# Portfolio-level findings
portfolio_findings = _portfolio_findings(products, total_revenue, quadrant_revenue, quadrant_eng)
return {
"company": data.get("company", "Unknown"),
"total_engineering_headcount": data.get("total_engineering_headcount"),
"products": products,
"total_revenue_quarterly": total_revenue,
"quadrant_summary": {
q: {
"count": sum(1 for p in products if p["quadrant"] == q),
"revenue": quadrant_revenue.get(q, 0),
"revenue_pct": quadrant_revenue.get(q, 0) / total_revenue if total_revenue else 0,
"eng_pct": quadrant_eng.get(q, 0),
}
for q in ["Star", "Cash Cow", "Question Mark", "Dog"]
},
"portfolio_health_score": health,
"portfolio_findings": portfolio_findings,
}
def _portfolio_health(products: list, total_revenue: float, total_eng: float) -> float:
"""
Portfolio health 0-1. Penalizes:
- No Stars (no growth engine)
- Dogs consuming > 20% of eng
- Poor alignment scores
- Revenue concentrated in Dogs/Question Marks
"""
score = 1.0
quadrants = [p["quadrant"] for p in products]
has_star = "Star" in quadrants
has_cash_cow = "Cash Cow" in quadrants
if not has_star:
score -= 0.25 # No growth engine is a serious problem
if not has_cash_cow:
score -= 0.10 # No cash generator means funding stars from burn
# Dog eng allocation penalty
dog_eng = sum(p["eng_capacity_pct"] for p in products if p["quadrant"] == "Dog")
if dog_eng > 20:
score -= 0.20
elif dog_eng > 10:
score -= 0.10
# Revenue in dogs penalty
if total_revenue > 0:
dog_rev_pct = sum(p["revenue_quarterly"] for p in products if p["quadrant"] == "Dog") / total_revenue
if dog_rev_pct > 0.30:
score -= 0.15
# Average alignment score
avg_alignment = sum(p["alignment_score"] for p in products) / len(products) if products else 0
score -= (1 - avg_alignment) * 0.20
return max(0.0, min(1.0, score))
def _portfolio_findings(
products: list, total_revenue: float,
quadrant_revenue: dict, quadrant_eng: dict
) -> list:
findings = []
stars = [p for p in products if p["quadrant"] == "Star"]
cows = [p for p in products if p["quadrant"] == "Cash Cow"]
questions = [p for p in products if p["quadrant"] == "Question Mark"]
dogs = [p for p in products if p["quadrant"] == "Dog"]
if not stars:
findings.append("✗ CRITICAL: No Star products. You have no growth engine. Identify a Question Mark to invest in or revisit your market positioning.")
elif len(stars) == 1:
findings.append(f"◑ Single Star ({stars[0]['name']}). Portfolio is fragile — one product drives all growth. Diversify.")
else:
findings.append(f"✓ {len(stars)} Star products — healthy growth engine.")
if not cows:
findings.append("⚠ No Cash Cow products. Stars are consuming capital without a self-funding mechanism. Watch burn rate.")
else:
cow_rev = quadrant_revenue.get("Cash Cow", 0)
cow_pct = cow_rev / total_revenue if total_revenue else 0
findings.append(f"✓ Cash Cow revenue: {cow_pct:.0%} of total — funds Star investment.")
if questions:
findings.append(f"⚠ {len(questions)} Question Mark(s): {', '.join(p['name'] for p in questions)}.")
findings.append(" Each needs a binary decision: invest to win share, or exit. Set a 2-quarter deadline.")
if dogs:
dog_eng_total = sum(p["eng_capacity_pct"] for p in dogs)
findings.append(f"✗ {len(dogs)} Dog product(s): {', '.join(p['name'] for p in dogs)} consuming {dog_eng_total}% of eng capacity.")
findings.append(f" That's {dog_eng_total}% of your engineers on declining products. Set sunset dates.")
# Alignment check
misaligned = [p for p in products if p["alignment_score"] < 0.50]
if misaligned:
findings.append(f"⚠ Engineering allocation misaligned on: {', '.join(p['name'] for p in misaligned)}.")
findings.append(" Rebalance: move capacity from Dogs/Cows to Stars.")
return findings
# ---------------------------------------------------------------------------
# Report rendering
# ---------------------------------------------------------------------------
def fmt_currency(n: float) -> str:
if n >= 1_000_000:
return f".1fM"
elif n >= 1_000:
return f".0fK"
return f".0f"
def render_report(result: dict) -> str:
lines = []
lines.append("=" * 65)
lines.append(f" PORTFOLIO ANALYZER — {result['company']}")
lines.append(f" Total Quarterly Revenue: {fmt_currency(result['total_revenue_quarterly'])}")
if result.get("total_engineering_headcount"):
lines.append(f" Engineering Headcount: {result['total_engineering_headcount']}")
lines.append("=" * 65)
lines.append("")
# Portfolio health
health = result["portfolio_health_score"]
bar_len = 40
filled = round(health * bar_len)
bar = "█" * filled + "░" * (bar_len - filled)
lines.append(f" Portfolio Health: {health:.0%}")
lines.append(f" [{bar}]")
lines.append("")
# Quadrant summary
lines.append(" QUADRANT SUMMARY")
lines.append(" " + "-" * 55)
header = f" {'Quadrant':<15} {'Count':>5} {'Revenue':>10} {'Rev%':>6} {'Eng%':>6}"
lines.append(header)
lines.append(" " + "-" * 55)
total_rev = result["total_revenue_quarterly"]
for q in ["Star", "Cash Cow", "Question Mark", "Dog"]:
qs = result["quadrant_summary"][q]
emoji = quadrant_emoji(q)
label = f"{emoji} {q}"
rev_pct = f"{qs['revenue_pct']:.0%}" if qs["count"] else "-"
eng = f"{qs['eng_pct']}%" if qs["count"] else "-"
rev = fmt_currency(qs["revenue"]) if qs["count"] else "-"
lines.append(f" {label:<15} {qs['count']:>5} {rev:>10} {rev_pct:>6} {eng:>6}")
lines.append("")
# Per-product breakdown
lines.append(" PRODUCT BREAKDOWN")
lines.append(" " + "-" * 65)
for p in result["products"]:
emoji = quadrant_emoji(p["quadrant"])
pc = posture_color(p["posture"])
lines.append(
f" {emoji} {p['name']} — {p['quadrant']} → {pc} {p['posture']}"
)
lines.append(
f" Revenue: {fmt_currency(p['revenue_quarterly'])}/qtr "
f"QoQ: {p['qoq_growth']:+.0%} "
f"Mkt growth: {p['market_growth_pct']:+.0f}%"
)
lines.append(
f" Share ratio: {p['share_ratio']:.1f}x "
f"Eng: {p['eng_capacity_pct']}% "
f"Alignment: {p['alignment_score']:.0%}"
)
if p.get("d30_retention") is not None:
lines.append(
f" D30 retention: {p['d30_retention']:.0%} "
f"NPS: {p['nps'] if p['nps'] is not None else 'N/A'}"
)
if p.get("notes"):
lines.append(f" Note: {p['notes']}")
for f in p.get("findings", []):
lines.append(f" {f}")
lines.append("")
# Portfolio-level findings
lines.append(" PORTFOLIO FINDINGS")
lines.append(" " + "-" * 65)
for f in result.get("portfolio_findings", []):
lines.append(f" {f}")
lines.append("")
lines.append("=" * 65)
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Portfolio Analyzer — BCG matrix classification and investment recommendations",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--input", "-i",
metavar="FILE",
help="JSON file with portfolio data (default: built-in sample data)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output raw JSON result",
)
args = parser.parse_args()
if args.input:
try:
with open(args.input) as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: file not found: {args.input}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
else:
print("No input file provided — running with sample data.\n")
data = sample_data()
result = analyze_portfolio(data)
if args.json:
# Make result JSON-serializable
def clean(obj):
if isinstance(obj, dict):
return {k: clean(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [clean(v) for v in obj]
elif isinstance(obj, float):
return round(obj, 4)
return obj
print(json.dumps(clean(result), indent=2))
else:
print(render_report(result))
if __name__ == "__main__":
main()
FILE:cro-advisor/SKILL.md
---
name: "cro-advisor"
description: "Revenue leadership for B2B SaaS companies. Revenue forecasting, sales model design, pricing strategy, net revenue retention, and sales team scaling. Use when designing the revenue engine, setting quotas, modeling NRR, evaluating pricing, building board forecasts, or when user mentions CRO, chief revenue officer, revenue strategy, sales model, ARR growth, NRR, expansion revenue, churn, pricing strategy, or sales capacity."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: cro-leadership
updated: 2026-03-05
python-tools: revenue_forecast_model.py, churn_analyzer.py
frameworks: sales-playbook, pricing-strategy, nrr-playbook
---
# CRO Advisor
Revenue frameworks for building predictable, scalable revenue engines — from $1M ARR to $100M and beyond.
## Keywords
CRO, chief revenue officer, revenue strategy, ARR, MRR, sales model, pipeline, revenue forecasting, pricing strategy, net revenue retention, NRR, gross revenue retention, GRR, expansion revenue, upsell, cross-sell, churn, customer success, sales capacity, quota, ramp, territory design, MEDDPICC, PLG, product-led growth, sales-led growth, enterprise sales, SMB, self-serve, value-based pricing, usage-based pricing, ICP, ideal customer profile, revenue board reporting, sales cycle, CAC payback, magic number
## Quick Start
### Revenue Forecasting
```bash
python scripts/revenue_forecast_model.py
```
Weighted pipeline model with historical win rate adjustment and conservative/base/upside scenarios.
### Churn & Retention Analysis
```bash
python scripts/churn_analyzer.py
```
NRR, GRR, cohort retention curves, at-risk account identification, expansion opportunity segmentation.
## Diagnostic Questions
Ask these before any framework:
**Revenue Health**
- What's your NRR? If below 100%, everything else is a leaky bucket.
- What percentage of ARR comes from expansion vs. new logo?
- What's your GRR (retention floor without expansion)?
**Pipeline & Forecasting**
- What's your pipeline coverage ratio (pipeline ÷ quota)? Under 3x is a problem.
- Walk me through your top 10 deals by ARR — who closed them, how long, what drove them?
- What's your stage-by-stage conversion rate? Where do deals die?
**Sales Team**
- What % of your sales team hit quota last quarter?
- What's average ramp time before a new AE is quota-attaining?
- What's the sales cycle variance by segment? High variance = unpredictable forecasts.
**Pricing**
- How do customers articulate the value they get? What outcome do you deliver?
- When did you last raise prices? What happened to win rate?
- If fewer than 20% of prospects push back on price, you're underpriced.
## Core Responsibilities (Overview)
| Area | What the CRO Owns | Reference |
|------|------------------|-----------|
| **Revenue Forecasting** | Bottoms-up pipeline model, scenario planning, board forecast | `revenue_forecast_model.py` |
| **Sales Model** | PLG vs. sales-led vs. hybrid, team structure, stage definitions | `references/sales_playbook.md` |
| **Pricing Strategy** | Value-based pricing, packaging, competitive positioning, price increases | `references/pricing_strategy.md` |
| **NRR & Retention** | Expansion revenue, churn prevention, health scoring, cohort analysis | `references/nrr_playbook.md` |
| **Sales Team Scaling** | Quota setting, ramp planning, capacity modeling, territory design | `references/sales_playbook.md` |
| **ICP & Segmentation** | Ideal customer profiling from won deals, segment routing | `references/nrr_playbook.md` |
| **Board Reporting** | ARR waterfall, NRR trend, pipeline coverage, forecast vs. actual | `revenue_forecast_model.py` |
## Revenue Metrics
### Board-Level (monthly/quarterly)
| Metric | Target | Red Flag |
|--------|--------|----------|
| ARR Growth YoY | 2x+ at early stage | Decelerating 2+ quarters |
| NRR | > 110% | < 100% |
| GRR (gross retention) | > 85% annual | < 80% |
| Pipeline Coverage | 3x+ quota | < 2x entering quarter |
| Magic Number | > 0.75 | < 0.5 (fix unit economics before spending more) |
| CAC Payback | < 18 months | > 24 months |
| Quota Attainment % | 60-70% of reps | < 50% (calibration problem) |
**Magic Number:** Net New ARR × 4 ÷ Prior Quarter S&M Spend
**CAC Payback:** S&M Spend ÷ New Logo ARR × (1 / Gross Margin %)
### Revenue Waterfall
```
Opening ARR
+ New Logo ARR
+ Expansion ARR (upsell, cross-sell, seat adds)
- Contraction ARR (downgrades)
- Churned ARR
= Closing ARR
NRR = (Opening + Expansion - Contraction - Churn) / Opening
```
### NRR Benchmarks
| NRR | Signal |
|-----|--------|
| > 120% | World-class. Grow even with zero new logos. |
| 100-120% | Healthy. Existing base is growing. |
| 90-100% | Concerning. Churn eating growth. |
| < 90% | Crisis. Fix before scaling sales. |
## Red Flags
- NRR declining two quarters in a row — customer value story is broken
- Pipeline coverage below 3x entering the quarter — already forecasting a miss
- Win rate dropping while sales cycle extends — competitive pressure or ICP drift
- < 50% of sales team quota-attaining — comp plan, ramp, or quota calibration issue
- Average deal size declining — moving downmarket under pressure (dangerous)
- Magic Number below 0.5 — sales spend not converting to revenue
- Forecast accuracy below 80% — reps sandbagging or pipeline quality is poor
- Single customer > 15% of ARR — concentration risk, board will flag this
- "Too expensive" appearing in > 40% of loss notes — value demonstration broken, not pricing
- Expansion ARR < 20% of total ARR — upsell motion isn't working
## Integration with Other C-Suite Roles
| When... | CRO works with... | To... |
|---------|------------------|-------|
| Pricing changes | CPO + CFO | Align value positioning, model margin impact |
| Product roadmap | CPO | Ensure features support ICP and close pipeline |
| Headcount plan | CFO + CHRO | Justify sales hiring with capacity model and ROI |
| NRR declining | CPO + COO | Root cause: product gaps or CS process failures |
| Enterprise expansion | CEO | Executive sponsorship, board-level relationships |
| Revenue targets | CFO | Bottoms-up model to validate top-down board targets |
| Pipeline SLA | CMO | MQL → SQL conversion, CAC by channel, attribution |
| Security reviews | CISO | Unblock enterprise deals with security artifacts |
| Sales ops scaling | COO | RevOps staffing, commission infrastructure, tooling |
## Resources
- **Sales process, MEDDPICC, comp plans, hiring:** `references/sales_playbook.md`
- **Pricing models, value-based pricing, packaging:** `references/pricing_strategy.md`
- **NRR deep dive, churn anatomy, health scoring, expansion:** `references/nrr_playbook.md`
- **Revenue forecast model (CLI):** `scripts/revenue_forecast_model.py`
- **Churn & retention analyzer (CLI):** `scripts/churn_analyzer.py`
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- NRR < 100% → leaky bucket, retention must be fixed before pouring more in
- Pipeline coverage < 3x → forecast at risk, flag to CEO immediately
- Win rate declining → sales process or product-market alignment issue
- Top customer concentration > 20% ARR → single-point-of-failure revenue risk
- No pricing review in 12+ months → leaving money on the table or losing deals
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Forecast next quarter" | Pipeline-based forecast with confidence intervals |
| "Analyze our churn" | Cohort churn analysis with at-risk accounts and intervention plan |
| "Review our pricing" | Pricing analysis with competitive benchmarks and recommendations |
| "Scale the sales team" | Capacity model with quota, ramp, territories, comp plan |
| "Revenue board section" | ARR waterfall, NRR, pipeline, forecast, risks |
## Reasoning Technique: Chain of Thought
Pipeline math must be explicit: leads → MQLs → SQLs → opportunities → closed. Show conversion rates at each stage. Question any assumption above historical averages.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:cro-advisor/references/nrr_playbook.md
# NRR Playbook
Net Revenue Retention is the single most important metric for a SaaS company's health and valuation. A company with 120% NRR grows even if it closes zero new deals. A company with 80% NRR is filling a bucket with a hole in it.
---
## NRR Deep Dive
### The Fundamental Formula
```
NRR = (Opening MRR + Expansion MRR - Contraction MRR - Churned MRR) / Opening MRR
Example:
Opening MRR: $1,000,000
Expansion: +$150,000
Contraction: -$30,000
Churn: -$80,000
Closing MRR: $1,040,000
NRR = $1,040,000 / $1,000,000 = 104%
```
### NRR vs. GRR
| Metric | Formula | What It Tells You |
|--------|---------|------------------|
| **GRR** | (Opening - Contraction - Churn) / Opening | Retention floor — how much you keep without any expansion |
| **NRR** | (Opening + Expansion - Contraction - Churn) / Opening | Net health — expansion offsetting churn |
| **Logo Retention** | (Customers start - Customers churned) / Customers start | Volume retention, ignores revenue weight |
**GRR is the floor. NRR is the ceiling.**
If GRR is 80% and NRR is 105%, your expansion is covering 25 points of churn. That's fragile — any expansion slowdown turns NRR negative. The fix is GRR, not more upsell.
### Benchmarks by Segment
| Segment | Good GRR | Good NRR | Exceptional NRR |
|---------|---------|---------|----------------|
| SMB-focused | 80-85% | 95-105% | > 110% |
| Mid-Market | 85-90% | 105-115% | > 120% |
| Enterprise | 90-95% | 115-130% | > 140% |
Enterprise NRR can exceed 140% because large accounts expand substantially and rarely churn entirely — they may downgrade but full logo churn is rare if the product is embedded.
### NRR by Cohort
Don't just measure NRR across the full base — measure it by customer cohort (month of acquisition).
```
Jan 2024 Cohort:
Opening MRR (Jan 2024): $50,000
MRR at Jan 2025: $62,000
12-month NRR: 124%
Feb 2024 Cohort:
Opening MRR (Feb 2024): $45,000
MRR at Feb 2025: $38,000
12-month NRR: 84% ← problem cohort
```
Cohort analysis reveals:
- Whether a specific acquisition channel brings lower-quality customers
- Whether a product change or pricing shift affected retention
- Whether specific sales reps or time periods created bad-fit deals
---
## Churn Anatomy
Not all churn is equal. Know the breakdown before prescribing solutions.
### Churn Types
| Type | Definition | Primary Cause | Fix |
|------|-----------|--------------|-----|
| **Logo churn** | Customer cancels entirely | No value, poor fit, champion left, competitor | Root cause analysis, ICP tightening |
| **Revenue churn** | ARR lost (cancels + downgrades combined) | Same as logo + downgrade triggers | Address both volume and revenue |
| **Involuntary churn** | Failed payment, expired card | Billing friction | Dunning improvement (quick win: 20-30% recovery) |
| **Voluntary churn** | Active cancellation decision | Explicit dissatisfaction, competitor win | Exit interview + intervention program |
| **Contraction** | Downgrade, seat reduction | Overpurchased, budget cut, team reduction | Right-sizing program, annual contracts |
### Churn Root Cause Framework
Run this analysis quarterly on all churned accounts:
**Step 1: Categorize by reason**
- No value realized (never activated or adopted)
- Value realized but budget cut (external, not product)
- Switched to competitor (why? what did they offer?)
- Champion left company (relationship loss, not product failure)
- Company shutdown / acquisition (unavoidable)
**Step 2: Look for patterns**
- Which ICP signals predict churn? (company size, vertical, acquisition channel)
- Which product behaviors predict churn? (no login in 30 days, never completed onboarding)
- Which time periods have highest churn? (months 3, 6, 12 are typical cliff points)
**Step 3: Act on the patterns**
- ICP pattern → tighten qualification criteria
- Behavior pattern → build early warning health score
- Time cliff → build intervention playbooks for months 2, 5, 11
### Exit Interview Protocol
Talk to every churned customer if ACV > $10K. For smaller, do quarterly batch surveys.
Questions:
1. "What was the primary reason for your decision to cancel?"
2. "What would have needed to be true for you to stay?"
3. "What did you switch to, and what drove that decision?"
4. "Was there a specific moment when you decided to leave?"
Rules:
- CSM who owned the account should NOT conduct the exit interview (too much relationship bias)
- Use a neutral party or the VP CS
- Document verbatim, not paraphrased
- Feed patterns back to Product and Sales monthly
---
## Customer Health Scoring
A health score predicts churn 60-90 days before it happens. Without one, you're reactive.
### Health Score Components
Score each account 0-100 across weighted signals:
| Signal | Weight | Red (0-33) | Yellow (34-66) | Green (67-100) |
|--------|--------|-----------|---------------|---------------|
| **Product usage** (DAU/WAU, feature adoption depth) | 35% | < 20% seats active | 20-60% seats active | > 60% seats active |
| **Engagement** (QBR attendance, champion responsiveness) | 20% | No response 60+ days | 30-60 days | Active, < 30 days |
| **NPS / CSAT** | 20% | Score < 6 | Score 6-7 | Score 8-10 |
| **Support volume** (negative signal: high volume = friction) | 15% | > 10 tickets/month | 3-10/month | < 3/month |
| **Contract signals** (time to renewal, expansion in motion) | 10% | < 60 days to renewal, no expansion discussion | 60-90 days, passive | > 90 days, expansion active |
**Composite score:**
- 70-100: Healthy. Renewal confident. Identify expansion opportunity.
- 50-69: At-risk. CSM check-in required. Executive sponsor loop-in if < 60 days to renewal.
- 0-49: Red alert. Immediate intervention. VP CS or CEO call if strategic account.
### Health Score Automation
Trigger alerts automatically:
```
Score drops > 20 points in 30 days → CSM immediate outreach (same day)
No product login in 14 days → Automated email + CSM flag (within 24 hours)
Champion leaves company → Executive outreach (within 24 hours)
Support escalation → CSM loop-in (within 2 hours)
Renewal < 90 days + score < 60 → VP CS review (weekly)
Seat utilization < 30% → Adoption intervention playbook triggered
```
### Leading Indicators vs. Lagging Indicators
| Leading (predict future churn) | Lagging (confirm past churn) |
|-------------------------------|------------------------------|
| Login frequency declining | Cancellation submitted |
| Feature adoption stalling at basic level | Non-renewal at contract end |
| NPS score trend (not just snapshot) | Downgrade executed |
| No QBR scheduled in 90+ days | Champion departure |
| Support escalations increasing | Competitor mentioned in support |
Build your health score from leading indicators. Lagging indicators tell you what already happened.
---
## Expansion Revenue Strategies
Expansion is cheaper than acquisition. CAC for expansion is typically 20-30% of new logo CAC.
### Expansion Motion 1: Seat Expansion
**Trigger signals:**
- Usage by unlicensed users (shared logins, "can you add my colleague?")
- Team growth visible on LinkedIn (company hiring in target department)
- Champion promotes to a new role with bigger team
- Power users at license limit consistently
**Playbook:**
1. Pull monthly usage report showing which features unlicensed users are using
2. Frame as: "Your team is getting value from X — you could be capturing that for the full team"
3. Offer a team expansion proposal at renewal + 10% volume discount for seat adds
4. Never penalize users for sharing logins before the conversation — that's a data asset
### Expansion Motion 2: Upsell (Tier Upgrade)
**Trigger signals:**
- Customer consistently hitting usage/feature limits
- Security or compliance requirement that requires higher tier
- New stakeholder joining who needs admin controls
- API usage growing rapidly (engineering team engagement)
**Playbook:**
1. Build a "value realized" report before the upsell conversation (ROI proof)
2. Use QBR as the venue: "You've achieved X. Here's what's possible at the next level."
3. Frame the upgrade as unlocking more of what's already working
4. Time to renewal: start upsell conversation 90-120 days before renewal
### Expansion Motion 3: Cross-sell
**Trigger signals:**
- Strategic account with adjacent problem your product can solve
- New product launch that complements existing usage
- Customer explicitly asks about a capability in your roadmap or adjacent product
**Playbook:**
1. Land with core product; build relationship and prove value
2. Cross-sell only after health score is green and NPS > 7
3. Introduce the new product through a champion, not a cold pitch
4. Pilot pricing: bundle into renewal at modest uplift vs. separate sale
5. Cross-sell owner: CSM or AE (define explicitly — joint ownership = no ownership)
### Expansion Sequencing
Don't try all three simultaneously. Sequence matters:
```
Month 0-3: Activation focus — ensure core value delivered
Month 3-6: Seat expansion — grow usage within existing team
Month 6-9: Upsell conversation — unlock advanced features
Month 9-12: Cross-sell OR renewal + multi-year lock-in
```
### NRR Modeling
Target breakdown for 115% NRR:
```
GRR: 88% (12% lost to churn/contraction)
Expansion rate: 27% (upsell + cross-sell + seat expansion)
NRR: 88% + 27% = 115%
To reach 120% NRR:
Option A: Improve GRR to 92% (reduce churn), keep expansion at 28%
Option B: Keep GRR at 88%, improve expansion to 32%
Option C: Both, incrementally
Option A is usually easier and more durable. Fix the hole first.
```
---
## Customer Success Integration
CS and Revenue are not separate functions. NRR lives at their intersection.
### CS Team Structure (aligned to NRR)
| CS Model | When to Use | NRR Focus |
|----------|------------|-----------|
| **High-touch CSM** | ACV > $25K | Named accounts, QBRs, executive relationships |
| **Tech-touch / pooled** | ACV $5K-25K | Automated health scoring, office hours, community |
| **Self-serve** | ACV < $5K | In-app guidance, knowledge base, email sequences |
**CSM coverage ratios:**
- High-touch: 1 CSM per $2M-4M ARR managed
- Tech-touch: 1 CSM per $5M-10M ARR managed
- Self-serve: Product and automation (no dedicated CSM)
### CS Compensation (aligned to NRR)
Don't pay CSMs a flat salary — align incentive to retention and expansion:
```
CS compensation structure:
Base: 70% of OTE
Variable: 30% of OTE
Variable tied to:
GRR / NRR vs. target (50% of variable)
Health score improvement (25% of variable)
Expansion ARR facilitated (25% of variable)
Do NOT pay CS commission on expansion ARR the same way AEs earn it.
This creates conflict: CS will push expansion before the customer is ready.
Instead, bonus for expansion milestones — it's a different incentive structure.
```
### QBR (Quarterly Business Review) Framework
QBRs are the primary vehicle for expansion and churn prevention in enterprise accounts.
**QBR agenda (60-90 minutes):**
1. **Their goals, our progress** — review what they said success looked like at kickoff (10 min)
2. **Usage and adoption data** — product metrics presented in business language, not feature language (15 min)
3. **Value delivered** — ROI proof: time saved, revenue influenced, risk reduced (10 min)
4. **Challenges and blockers** — what's preventing more adoption? (10 min)
5. **Roadmap preview** — upcoming features relevant to their use case (10 min)
6. **Next 90 days** — joint success plan with owner and due dates (10 min)
7. **Expansion opportunity** — if health score is green and timing is right (10 min)
**QBR anti-patterns:**
- Leading with your product roadmap (they don't care; start with their results)
- Bringing too many people from your side without matching seniority
- Presenting at a VP without bringing the economic buyer
- Skipping QBRs for "healthy" accounts (health can change fast)
- No confirmed next step at the end
---
## Cohort-Based Retention Analysis
Aggregate NRR hides the signal. Cohort analysis reveals it.
### Retention Curve Analysis
Plot retention by months since acquisition for each quarterly cohort:
```
Month 0: 100% (starting revenue)
Month 3: First cliff — early adopters who didn't activate churn here
Month 6: Second cliff — customers who never expanded, running out of runway
Month 12: Renewal cliff — annual contract renewal decision
Month 18: Mature customers — churn rate stabilizes significantly
Healthy curve: Drops sharply in months 1-3, flattens after month 6
Problem curve: Continues declining linearly through month 12+ (no value anchor)
```
### Reading Cohort Data
| Pattern | Interpretation | Action |
|---------|---------------|--------|
| Early churn (months 1-3) | Onboarding / activation failure | Fix time-to-value, improve onboarding |
| Mid-cycle churn (months 4-8) | Value not deepening | Adoption program, check product fit |
| Annual renewal churn (month 12) | Buying committee didn't renew | Executive engagement, earlier renewal process |
| Flat after month 6 | Sticky product, low expansion | Increase upsell motion |
| Growing after month 6 | Expansion working | Scale the upsell playbook |
### Cohort Segmentation Variables
Slice retention cohorts by:
- **Acquisition channel** (inbound vs. outbound vs. PLG vs. partner)
- **Sales rep** (which reps close durable deals vs. churny deals)
- **Deal size** (SMB churn rate typically 2-3x enterprise)
- **Industry vertical** (some verticals have structurally higher churn)
- **Product tier at signup** (self-serve → converted vs. directly contracted)
- **Geographic market** (international markets often have different retention profiles)
The most actionable finding is usually by acquisition channel or sales rep — both are directly controllable.
### Churn Prevention Intervention Playbooks
**Playbook 1: Low Activation (no login in first 14 days)**
```
Day 7: Automated email: "Getting started" + specific next step
Day 14: CSM outreach: "I noticed you haven't logged in — can I help?"
Day 21: Escalate to CSM manager if no response
Day 30: Executive outreach for ACV > $25K; flag as at-risk
```
**Playbook 2: Usage Cliff (DAU drops > 50% in 30 days)**
```
Trigger: Automated health score alert
Day 1: CSM reviews usage report, identifies likely cause
Day 2: CSM outreach: "We noticed your team's usage changed — is everything okay?"
Day 7: If no response: schedule 30-min call with champion
Day 14: If unresponsive: VP CS loop-in + executive reach out
```
**Playbook 3: Champion Departure**
```
Trigger: LinkedIn alert or internal report of champion leaving
Day 1: Email to departed champion (warm handoff ask)
Day 1: Email to new stakeholder (introduction from AE or VP CS)
Day 3: Schedule onboarding call for new stakeholder
Day 14: QBR with new stakeholder to establish relationship
Day 30: Health score review — flag if engagement hasn't recovered
```
**Playbook 4: Pre-Renewal (90 days out, health score < 70)**
```
Day -90: CSM completes account health review, escalates if < 70
Day -75: Executive sponsor from vendor side joins renewal call
Day -60: Value delivered report prepared (ROI proof)
Day -45: Renewal proposal sent with expansion option
Day -30: Follow-up on any open objections or requirements
Day -14: Final confirm or escalate to VP Sales
```
FILE:cro-advisor/references/pricing_strategy.md
# Pricing Strategy
Pricing is not a one-time decision. It's an ongoing hypothesis about value and willingness to pay. Most SaaS companies are underpriced by 20-40%.
---
## Pricing Models
### Per Seat / User
**How it works:** Customer pays a fixed amount per user, per month or year.
**Best for:**
- Collaboration tools (everyone who uses it needs a license)
- Productivity software where value scales with users
- Products where you want viral / network growth within accounts
**Pricing structure:**
```
Starter: $15/user/month (1-10 users)
Professional: $30/user/month (11-100 users)
Enterprise: Custom (100+ users, negotiated)
```
**Pros:**
- Simple to understand and sell
- Revenue scales naturally with customer growth
- Predictable for customers (fixed monthly cost)
**Cons:**
- Customers negotiate volume discounts aggressively
- Discourages broad adoption if price is high (seat hoarding)
- Doesn't capture value for power users vs. light users
- Enterprises can negotiate $5/seat on a $25 product
**Watch for:** Customers sharing logins to avoid per-seat cost. Enforce with IP restrictions or SSO audit logs.
---
### Usage-Based Pricing (UBP)
**How it works:** Customer pays for what they consume — API calls, data processed, messages sent, compute hours, etc.
**Best for:**
- API companies, infrastructure, data platforms
- AI products (per-token, per-query pricing)
- Products where value scales non-linearly with usage
- Land-and-expand: low entry cost, grows with customer success
**Pricing structure:**
```
Free tier: First 10K API calls/month
Pay-as-you-go: $0.002 per API call
Committed use: $500/month for 500K calls (better rate)
Enterprise: Custom contract, committed volume discount
```
**Pros:**
- Customer pays in proportion to value received
- Low barrier to entry (customers start small, scale up)
- Natural expansion: customer success = revenue growth
- No "unused licenses" problem
**Cons:**
- Revenue is unpredictable for both you and the customer
- Hard to forecast; hard to budget for customer
- Customers may optimize to reduce usage (and your revenue)
- Complex billing; requires robust usage tracking infrastructure
**Usage-based pricing math:**
```
Unit cost (your COGS per unit): $0.0002 per API call
Target gross margin: 80%
Price = COGS / (1 - margin) = $0.0002 / 0.20 = $0.001 minimum
Add markup for value delivered above cost: $0.002 per call (10x markup at scale)
```
**Hybrid usage + seat approach:**
- Platform fee: $500/month (access, support, base features)
- Usage fee: $0.001 per API call above included 100K
---
### Flat Rate / Subscription
**How it works:** One price for full access, regardless of usage or users.
**Best for:**
- Simple products with limited feature differentiation
- Products where usage is predictable and bounded
- Customers who want budget certainty
- Early stage before you've figured out value segmentation
**Pros:**
- Simplest to sell and explain
- Easiest billing implementation
- Customers love budget predictability
**Cons:**
- Leaves money on the table for heavy users
- No natural expansion revenue mechanism
- Light users pay the same as power users (retention risk)
**When to move away from flat rate:**
- 20% of customers are using 80% of the product capacity
- Power users would clearly pay more; light users churn or underutilize
- You have a clear expansion story waiting to happen
---
### Tiered / Feature-Based
**How it works:** Multiple packages (Starter, Pro, Enterprise) with different feature sets and/or usage limits.
**Best for:**
- Multi-use-case products
- Different buyer types (individual vs. team vs. enterprise)
- Products with a natural upgrade path based on sophistication
**Structure (Good / Better / Best):**
```
Starter ($49/mo): Core features, 3 users, 10GB storage
Professional ($149/mo): Advanced features, 25 users, 100GB, API access
Business ($499/mo): All features, 100 users, 1TB, SSO, priority support
Enterprise (custom): Unlimited, custom integrations, SLA, dedicated CSM
```
**Tier design principles:**
- Starter tier: removes friction, proves value, not the revenue center
- Professional: the primary revenue tier; 60-70% of customers land here
- Enterprise: custom pricing allows you to capture maximum value
- Each tier upgrade should have an obvious "must-have" feature for the target buyer
**What to gate on each tier:**
| Feature Type | Where to Put It |
|-------------|----------------|
| Core product functionality | Starter (must be useful) |
| Collaboration features | Pro (drives team usage) |
| Admin, security, SSO | Business/Enterprise |
| API / integrations | Pro and above |
| SLAs, dedicated support | Enterprise only |
| Advanced analytics | Business/Enterprise |
---
### Hybrid Pricing
**How it works:** Combination of models (e.g., platform fee + per seat + usage).
**Example:**
```
Platform fee: $2,000/month (access, core features, admin console)
Per seat: $50/user/month (up to 200 users)
Usage overage: $0.10/action above 100K included actions
```
**When to use hybrid:**
- Enterprise customers want budget certainty (platform fee) but your value scales with usage
- You have different cost structures for different features
- Customers have very different usage patterns across the base
**Pros:** Captures value at multiple dimensions. Hybrid is most common in enterprise SaaS.
**Cons:** More complex to explain and bill. Sales training burden increases.
---
## Value-Based Pricing Methodology
Cost-plus pricing is a race to the bottom. Price on value, not cost.
### Step 1: Define the Economic Outcome
What business result does your product deliver? Be specific.
**Weak:** "We help companies save time"
**Strong:** "We reduce onboarding time for new enterprise software by 40%, saving 8 hours per employee"
Map to one of:
- **Revenue increase** — "Our customers close 25% more deals using our CRM intelligence"
- **Cost reduction** — "We eliminate 60% of manual data entry for finance teams"
- **Risk reduction** — "We reduce compliance violations by 90%, avoiding $500K+ in potential fines"
- **Time savings** — "CSMs spend 5 fewer hours per week on manual reporting"
### Step 2: Quantify Per Customer
Calculate the dollar value of the outcome for your average customer.
```
Example: Data entry automation product
Target customer: 50-person finance team
Manual data entry: 4 hours/person/week
Hours saved with product: 2.4 hours/person/week (60% reduction)
Fully loaded cost of finance analyst: $75/hour
Weekly savings: 50 employees × 2.4 hours × $75 = $9,000
Annual savings: $9,000 × 52 weeks = $468,000
```
### Step 3: Determine Willingness to Pay
Customers will typically pay 10-20% of the value delivered for software.
```
Annual value delivered: $468,000
Willingness to pay range: $46,800 - $93,600/year
Current market pricing: ~$60,000/year
Your pricing: $72,000/year (between median and upper WTP)
```
**Test your hypothesis:**
- Interview 5-10 customers: "If we charged $X/year, is that reasonable?"
- Van Westendorp Price Sensitivity Meter:
- "At what price is this too cheap to trust?"
- "At what price is this a good deal?"
- "At what price is this getting expensive but still worth it?"
- "At what price is this too expensive?"
### Step 4: Validate with Win Rate Analysis
```
Run this analysis quarterly:
Track win rate by price point (segmented if possible)
Win rate 30-40%: pricing is likely right
Win rate < 20%: price is too high OR value demonstration is broken
Win rate > 50%: you're underpriced
Note: Distinguish between "lost on price" and "lost on fit."
Lost on price + good ROI proof: test lower price or improve value story
Lost on fit: ICP problem, not pricing problem
```
---
## Packaging (Good / Better / Best)
### The Three-Package Framework
Packaging is not just about features. It's about serving different buyer personas with different budgets and needs.
**Buyer personas by tier:**
```
Starter → The individual contributor or small team trying to solve an immediate problem
- Low budget authority
- Low-friction purchase (credit card, self-serve)
- Needs quick time to value
Professional → The team manager or department head
- $10K-100K budget authority
- Works with inside sales
- Needs collaboration features and reporting
Enterprise → The VP or C-suite buyer
- Unlimited budget (but requires justification)
- Needs compliance, security, SLAs, dedicated support
- Long buying process, multiple stakeholders
```
### Packaging Design Rules
1. **Each tier must be useful on its own.** Starter can't be crippled—customers need to succeed.
2. **Upgrade triggers must be obvious.** When a customer hits a limit, the next tier should solve it clearly.
3. **Don't gate features that drive adoption.** Collaboration features gated in a low tier kill viral growth.
4. **Enterprise pricing is custom.** Show "Contact Sales" or a starting price. Don't publish a firm enterprise price—you'll anchor too low.
5. **Annual vs. monthly pricing:** Charge 15-25% more for monthly vs. annual. Incentivize annual prepay.
### Pricing Page Design
- Lead with the most popular tier (visually prominent)
- Show annual pricing by default (with toggle to monthly)
- Highlight one or two "recommended" plans
- Feature comparison table: minimize the number of rows (overwhelm = no decision)
- Show logos of customers on each tier (social proof by segment)
- Live chat for enterprise CTA, not "Contact Sales" form
---
## Pricing Experiments and Rollout
### Before You Change Pricing
**Internal checklist:**
- [ ] Validate new pricing with 5-10 current customers (interviews)
- [ ] Run a willingness-to-pay survey with 50+ prospects
- [ ] Model revenue impact: how many customers at new pricing are equivalent to current ARR?
- [ ] Get CFO sign-off on cash flow impact
- [ ] Prepare messaging for customers, website, sales team
- [ ] Set a rollout date 60-90 days out
### Testing Approaches
**Cohort testing (safest):**
- New signups see new pricing; existing customers are grandfathered
- Monitor: conversion rate, ACV, win rate, time-to-close
- Run for 90 days before full rollout
**A/B pricing test (higher stakes):**
- Half of new signups see price A, half see price B
- Risk: word gets out that prices differ (customer frustration)
- Use only on self-serve, where purchase is not sales-assisted
**Segment-specific rollout:**
- Change pricing in one segment (e.g., SMB) while holding enterprise steady
- Lower risk than full rollout; validate before expanding
### Pricing Rollout Plan
```
Day 0: Decision made, pricing document approved
Day -60: Internal communication to sales, CS, support
Day -45: Customer communication drafted and reviewed
Day -30: New pricing live on website for new customers
Day -30: Existing customer email sent (90-day grandfather period)
Day -30: Sales team trained, FAQ document ready
Day -14: Second reminder to existing customers
Day 0: Existing customers transition to new pricing
Day +30: Win rate analysis, NRR impact review
```
### Grandfathering Policy
- **Standard:** Grandfather existing customers at old price for 12 months
- **Aggressive:** 90 days grandfather, then new pricing applies (use if you're raising significantly)
- **Never:** Retroactive pricing changes with no notice. This is a churn trigger and brand damage.
Grandfathering message framing:
> "We're investing significantly in [feature areas]. As a valued customer, your pricing remains unchanged through [date]. After that, your new rate will be $X — still X% less than new customer pricing as a thank-you for your partnership."
---
## Competitive Pricing Analysis
### Mapping the Competitive Landscape
```
Step 1: List all direct competitors
Step 2: Find their public pricing (website, G2, Capterra)
Step 3: Secret shop their sales process for unpublished pricing
Step 4: Talk to customers who considered them ("What did they quote you?")
Step 5: Map to your packaging (apples-to-apples comparison)
Output: Competitive pricing matrix
You: $X/month per seat at Pro tier
Competitor A: $Y/month per seat at equivalent tier
Competitor B: Custom (enterprise only)
```
### Competitive Positioning by Price
| Your Position | Situation | Response |
|--------------|-----------|---------|
| Significantly cheaper | Unclear why | Raise prices or clarify differentiation |
| Slightly cheaper | Winning on price | Test raising price, monitor win rate |
| At market | Competing on features | Make sure differentiation is clear in sales |
| Slightly more expensive | Win rate healthy | Price is justified by value |
| Significantly more expensive | Win rate low | Improve value proof or re-examine ICP |
### When "They're Cheaper" Appears in Deals
**Coach your reps:**
1. "What makes [Competitor] worth choosing over the $X difference?" (reframe value, not price)
2. "If price were equal, which would you choose and why?" (understand true preference)
3. "What's the cost of not solving this problem in Q3?" (urgency + value)
4. "What's their implementation cost and time?" (TCO, not ACV)
**If price is truly the barrier:**
- Offer a pilot at reduced scope (not price) to prove value
- Multi-year deal with year-one discount
- Defer payment to match their budget cycle (start in Q4, bill in Q1)
- Confirm it's price and not a champion issue or lack of urgency
---
## When to Raise Prices
### Green Lights for a Price Increase
**Product signals:**
- Customer usage growing QoQ (product delivers real value)
- NPS consistently > 40
- Feature requests indicate you're solving critical workflows
- Customers measuring and can articulate ROI
**Market signals:**
- Win rate > 35% (strong signal of underpricing)
- Waitlist or high inbound conversion without price objections
- Competitors raising prices (market is moving up)
- You've added significant value (new features, integrations, uptime improvements)
**Business signals:**
- Gross margin below 70% (cost inflation requires pricing response)
- CAC payback > 24 months (need higher ACV to fix unit economics)
- Haven't raised prices in 2+ years (inflation alone justifies adjustment)
### How Much to Raise
**Conservative:** 10-15% increase. Low risk, low disruption.
**Standard:** 15-30% increase. Acceptable if value story is strong.
**Aggressive:** 30-50% increase. Only with major product investment or clear underprice.
**Repositioning:** 2-5x increase. Rare; requires moving to a new buyer persona.
**Rule:** If fewer than 20% of prospects mention price as a concern, you're underpriced. Test.
### Price Increase Execution
1. Raise new business pricing immediately on the website
2. Communicate to existing customers with 90 days notice
3. Grandfather for 12 months OR give a 10-15% loyalty discount on new price
4. Track: conversion rate (new business), churn rate (existing), expansion ARR impact
5. Monitor win rate for 60 days post-increase; adjust if win rate drops > 5 points
**What not to do:**
- Don't apologize for raising prices
- Don't over-explain the justification (confident framing wins)
- Don't let sales reps negotiate discounts back to old pricing "just this once"
- Don't raise prices and remove features simultaneously
FILE:cro-advisor/references/sales_playbook.md
# Sales Playbook
Frameworks for building, running, and scaling a B2B SaaS sales organization.
---
## Sales Process Design
A sales process is a repeatable series of steps that takes a prospect from first contact to closed revenue. Without it, you have individual heroics, not a scalable machine.
### The Core Funnel
```
Lead Generation → Qualification → Discovery → Demo → Trial / POC → Proposal → Negotiation → Close → Handoff
```
Each stage has a clear entry criterion, exit criterion, and owner.
### Stage Definitions
#### Stage 0: Lead / Suspect
- **Entry:** Contact exists in CRM with basic firmographic data
- **Owner:** Marketing or SDR
- **Exit criterion:** Meets ICP criteria (company size, industry, tech stack)
- **Action:** Research, prioritize, add to outbound sequence
#### Stage 1: Prospecting / Outreach
- **Entry:** ICP-qualified account, no contact yet
- **Owner:** SDR or AE (depending on model)
- **Exit criterion:** Meeting booked with a qualified contact
- **Action:** Multi-channel outreach (email + call + LinkedIn), 8-12 touch sequence
- **Key metric:** Meeting booked rate (benchmark: 2-5% of outbound contacts)
#### Stage 2: Discovery
- **Entry:** First meeting confirmed
- **Owner:** AE (SDR hands off or joins)
- **Exit criterion:** Confirmed: pain, budget range, decision process, timeline
- **Action:** Ask questions. Listen. Map the org. Don't pitch yet.
- **Key metric:** Discovery-to-demo rate (benchmark: 60-80% proceed)
**Discovery question framework:**
```
Situation: "How do you currently handle [problem area]?"
Problem: "What's the impact when [pain point] happens?"
Implication: "If this continues, what does that mean for [business goal]?"
Need-payoff: "If we solved this, what would that be worth to you?"
```
#### Stage 3: Demo / Solution Presentation
- **Entry:** Confirmed pain and fit from discovery
- **Owner:** AE (+ SE for complex products)
- **Exit criterion:** Prospect agrees to evaluate / trial; next step defined
- **Action:** Show the workflow that solves their specific pain (not a feature tour)
- **Key metric:** Demo-to-trial/proposal rate (benchmark: 40-60%)
**Demo structure:**
1. Recap their pain (show you listened) — 5 min
2. Show the "aha moment" (fastest path to value) — 10 min
3. Walk the specific workflow they described — 15 min
4. Handle objections, confirm fit — 5 min
5. Define clear next step (date, owners, criteria) — 5 min
Never show features they didn't ask for. Every additional feature is noise until they have a reason to care.
#### Stage 4: Trial / POC
- **Entry:** Prospect commits to evaluate with real data/use case
- **Owner:** AE + CSM or SE
- **Exit criterion:** Success criteria met, POC success confirmed
- **Action:** Define success criteria upfront (in writing). Set a tight timeframe (2-4 weeks max).
- **Key metric:** POC-to-proposal rate (benchmark: 50-70%)
**POC setup requirements:**
```
Before any POC:
□ Signed NDA
□ Written success criteria ("We'll move forward if X happens")
□ Named champion who owns the evaluation
□ Executive sponsor identified
□ Defined timeline with end date
□ Agreed next step if criteria are met
```
If you can't get written success criteria, you don't have a real opportunity. You have a "we'll see."
#### Stage 5: Proposal / Pricing
- **Entry:** POC success OR strong discovery fit for simple products
- **Owner:** AE
- **Exit criterion:** Proposal received, timeline to decision confirmed
- **Action:** Present in a live call, never email a proposal cold
- **Key metric:** Proposal-to-negotiation rate (benchmark: 50-75%)
**Proposal structure:**
1. Problem statement (their words, not yours)
2. Proposed solution (mapped to their workflow)
3. ROI summary (value delivered vs. investment)
4. Pricing options (give 2-3 options; anchors the decision)
5. Next steps with dates
#### Stage 6: Negotiation
- **Entry:** Verbal intent to proceed, price/terms discussion begins
- **Owner:** AE (+ VP Sales for large deals)
- **Exit criterion:** Mutual agreement on terms; contract sent
- **Action:** Never discount before they ask. Discount on scope, not on margin.
- **Key metric:** Negotiation win rate (benchmark: 70-85%)
**Negotiation principles:**
- Get something for everything you give. Discount → multi-year. Fast close → early pay discount.
- Don't negotiate against yourself. Silence after an offer is not rejection.
- Know your walk-away before you enter. If you don't have a BATNA, you have no leverage.
- Legal/procurement delay ≠ deal death. Keep the champion engaged.
#### Stage 7: Close
- **Entry:** Signed contract or PO received
- **Owner:** AE
- **Exit criterion:** Contract countersigned, kickoff date set
- **Action:** Celebrate with the customer. Immediately introduce CSM.
- **Key metric:** Average close rate (closed won ÷ all closed = won + lost)
#### Stage 8: Handoff to Customer Success
- **Entry:** Deal closed
- **Owner:** AE + CSM
- **Exit criterion:** Customer has met their assigned CSM, kickoff scheduled
- **Action:** Internal handoff call with AE + CSM. AE shares: deal context, key stakeholders, use case, success criteria, any promises made during the sale.
**Handoff document (AE fills before first CS meeting):**
```
Account: [name]
ACV: $X
Close date: [date]
Primary contact: [name, title, email]
Economic buyer: [name, title]
Use case: [specific workflow]
Success criteria: [what they said good looks like in 90 days]
Promises made: [anything specific committed during sale]
Risk flags: [competitive, budget, champion strength]
```
---
## MEDDPICC Qualification Framework
MEDDPICC is the enterprise qualification standard. If you can't answer every letter, you don't have a qualified opportunity — you have a conversation.
### M — Metrics
What is the quantified business impact? What does winning look like in numbers?
- "What's the current cost of [the problem]?"
- "How do you measure success in this area today?"
- "If we achieve X outcome, what does that save or earn you?"
**Red flag:** No metrics = no business case = hard to get budget.
### E — Economic Buyer
Who has final authority to approve the budget?
- "Who else will be involved in the final decision?"
- "Have you purchased solutions in this range before? Who approved that?"
- "When we get to final terms, who needs to sign?"
**Red flag:** You only know the user buyer. Economic buyer hasn't engaged.
### D — Decision Criteria
What factors will they use to evaluate and select a solution?
- "What's most important in your evaluation?"
- "How will you compare options?"
- "What does the ideal solution look like to you?"
**Why it matters:** If you don't know their criteria, you're guessing what to prove. Define the criteria before you compete on them.
### D — Decision Process
What are the steps from evaluation to signed contract?
- "Walk me through your process from here to signed agreement."
- "Does procurement get involved? Legal? InfoSec?"
- "Have you purchased software at this price before? How long did that take?"
**Red flag:** No defined process = unlimited sales cycle.
### P — Paper Process
What's the contract and legal process?
- "Who manages vendor contracts on your side?"
- "What's your standard MSA, or do you use ours?"
- "How long does legal review typically take?"
**Why it matters:** Legal and procurement have killed many "done" deals. Start early. Route to your legal team simultaneously.
### I — Identify Pain
What is the specific, felt pain driving this evaluation?
- "What triggered this initiative now vs. six months ago?"
- "What happens if you don't solve this in Q3?"
- "On a scale of 1-10, how urgent is this for your team?"
**Red flag:** Pain isn't felt by the economic buyer. User pain ≠ budget authority.
### C — Champion
Who will actively sell your solution internally when you're not in the room?
- "Who else have you brought into this evaluation?"
- "Can you help us get access to [economic buyer / IT / security]?"
- "If the decision went the wrong way, who would be disappointed?"
**Red flag:** Your champion is enthusiastic but has no internal influence.
### C — Competition
Who else are they evaluating? What's your position?
- "Are you looking at alternatives?"
- "What made you start with us?"
- "Have you used [Competitor X] before?"
**Why it matters:** Knowing the competitive field tells you what you need to prove and what to neutralize.
### MEDDPICC Scorecard
| Letter | Score 1 | Score 2 | Score 3 |
|--------|---------|---------|---------|
| Metrics | No numbers | Approximate value | Specific ROI model |
| Economic Buyer | Unknown | Named, not engaged | Engaged directly |
| Decision Criteria | Vague | Partially defined | Written, weighted |
| Decision Process | Unknown | Verbal description | Steps confirmed, timeline known |
| Paper Process | Unknown | Basic awareness | Legal contacts, standard process known |
| Identify Pain | No urgency | User-level pain | Executive-level pain with consequences |
| Champion | No advocate | Friendly contact | Actively selling internally |
| Competition | Unknown | Identified | Position mapped, differentiation clear |
**Score each 1-3. Total 16+/24 = qualified opportunity. Under 12 = unqualified, do not forecast.**
---
## Sales Compensation Plans
Comp drives behavior. Design it precisely.
### Base / Variable Split
| Role | Base % | Variable % | Rationale |
|------|--------|-----------|-----------|
| SDR | 60-70% | 30-40% | Activity-based, not purely revenue |
| AE (Inside Sales) | 50% | 50% | Balanced risk/reward |
| AE (Enterprise) | 55-60% | 40-45% | Longer cycle, higher base for stability |
| VP Sales | 50% | 50% | Accountable for team results |
| CSM (retention focus) | 70% | 30% | Less variable, stable relationship role |
| CSM (expansion focus) | 60% | 40% | Expansion quota adds variable |
### Commission Structure
**Standard AE plan:**
```
Base: $80K
Variable: $80K (at 100% quota attainment)
OTE: $160K
Commission rate: OTE variable ÷ Quota
If quota = $800K ARR: commission = $80K ÷ $800K = 10% of ARR closed
Accelerators (performance above quota):
101-125% quota: 1.25x commission rate (12.5% of ARR)
126-150% quota: 1.5x commission rate (15% of ARR)
> 150% quota: 2.0x commission rate (20% of ARR)
```
**Why accelerators matter:**
- They keep top performers motivated past quota
- They make it possible for top reps to earn $200K+ (attracting talent)
- They create the "make it rain" culture
### SDR Compensation
SDRs are measured on output (meetings booked, pipeline created), not closed revenue.
```
Quota: 20 qualified meetings booked per month (or $X pipeline created)
Commission: $150-300 per qualified meeting held
Accelerators:
If a meeting converts to closed won: Bonus $250-500
If monthly meetings > 125% of quota: 1.5x rate on upside meetings
```
### Clawbacks
A clawback recovers commission paid on deals that churn or are fraudulently closed.
**Common clawback rules:**
- Full clawback if customer cancels within 90 days of close
- 50% clawback if customer cancels within 91-180 days
- No clawback after 180 days (AE shouldn't be penalized for future CS failures)
- Clawbacks vest: pay commission immediately but apply against next quarter's payout if triggered
**Why clawbacks matter:**
- Without them, reps are incentivized to close any deal, regardless of fit
- With them, reps self-qualify more carefully
### SPIFFs (Sales Performance Incentive Funds)
Short-term tactical incentives for specific behaviors:
- $5K bonus for closing a new vertical deal this quarter
- 1.5x commission on annual prepay deals in Q4
- $1K for closing a deal in a new geographic territory
Use SPIFFs sparingly. Overuse trains reps to wait for the SPIFF before engaging.
### Multi-Year and Prepay Incentives
Align rep behavior with company cash flow:
- Multi-year deals: Credit full TCV against quota, pay commission upfront on TCV
- Annual prepay: 10-20% uplift on commission rate
- Monthly billing: Standard commission rate
---
## Enterprise vs. SMB vs. Self-Serve Models
### Self-Serve / PLG
**Characteristics:**
- Product is the primary acquisition channel
- Credit card required (no invoicing)
- No human touch in the initial purchase
- Sales engages only at enterprise signals (high usage, team expansion, compliance needs)
**Funnel:**
```
Website → Free trial / Freemium → Activation → PQL → Expansion → Enterprise
```
**Key metrics:**
- Free-to-paid conversion rate (benchmark: 2-5% of signups)
- Time to activation (first core action)
- PQL → expansion conversion rate
- NRR from self-serve base
**Sales involvement triggers (PQL signals):**
- Team size > 10 seats
- Usage spikes (power user patterns)
- Feature limit hits on core features
- Job title change (new economic buyer appears in account)
### SMB Inside Sales
**Characteristics:**
- ACV $5K-25K
- 30-60 day sales cycle
- Inbound-heavy or light outbound
- SDR → AE → CS model
- Phone + email + video; no in-person
**Funnel:**
```
Inbound/MQL → SDR qualifies → AE discovery → Demo → Proposal → Close
```
**Key metrics:**
- MQL-to-SQL rate (benchmark: 15-25%)
- SQL-to-close rate (benchmark: 20-30%)
- Average sales cycle (30-60 days)
- AE productivity: $600K-$1M quota per rep
**Team ratios:**
- 1 SDR supports 3-4 AEs
- 1 CSM manages $1M-2M ARR
### Enterprise Sales
**Characteristics:**
- ACV $50K+
- 90-365 day sales cycle
- Outbound prospecting + inbound from brand
- AE + SE + executive sponsor model
- Multi-stakeholder: champion, economic buyer, IT, legal, procurement
**Funnel:**
```
Account targeting → Executive outreach → Discovery → POC → Security review → Legal → Procurement → Close
```
**Key metrics:**
- Deals in pipeline (volume matters less, quality more)
- POC win rate (benchmark: 60-75%)
- Average sales cycle (3-12 months)
- AE productivity: $1.5M-$3M quota per rep
**Team ratios:**
- 1 SE supports 3-4 AEs
- 1 CSM manages $2M-5M ARR (named accounts, high-touch)
---
## Sales Hiring and Ramp
### What "Good" Looks Like by Role
**SDR (entry level):**
- 1-2 years of outbound experience OR strong track record in customer-facing role
- Resilient: rejection is the job
- Coachable: SDR is a proving ground, not a final destination
- Can write clear, concise prospecting emails without templates
**AE (inside sales):**
- 2-4 years sales experience, preferably SaaS
- Can articulate their process for a discovery call
- Knows their numbers: quota, attainment, average deal size, sales cycle
- Shows how they build pipeline (AEs who only work inbound are a risk)
**AE (enterprise):**
- 4-8 years B2B sales, at least 2 in enterprise
- Has closed deals > $100K ACV
- Can name the stakeholders in a complex deal they navigated
- Understands procurement, security review, multi-year contracts
**VP Sales:**
- Has scaled a team from where you are to 2x your size
- Can build a comp plan from scratch
- Has hiring and firing experience
- Revenue from a repeatable process, not personal relationships
### Interview Process
**3-stage process:**
1. **Recruiter screen** (30 min): Motivation, experience, logistics
2. **Manager interview** (60 min): Structured questions on process, examples, numbers
3. **Panel / role play** (90 min): Mock discovery call + debrief; team fit
**Role play rubric:**
- Did they prepare (knew your product, your ICP)?
- Did they ask before pitching?
- Did they handle pushback without capitulating immediately?
- Did they confirm a next step with a date?
### Onboarding Structure (6-Week Ramp)
| Week | Focus | Activities |
|------|-------|-----------|
| 1 | Company, product, ICP | Onboarding sessions, product sandbox, shadow AE calls |
| 2 | Sales process, tools, messaging | CRM training, call review, write first prospecting emails |
| 3 | First outreach | Send first sequences, book first meetings, shadow closes |
| 4 | Independent discovery | Lead own discovery calls with manager reviewing |
| 5 | Full cycle | Handle pipeline independently, weekly coaching |
| 6 | Quota-bearing | 25% of quota expectation; full accountability begins |
### Performance Management
**Clear standards, no surprises:**
```
Month 3: 25% of quota expected. Miss by > 50% → performance conversation.
Month 4: 50% of quota expected. Miss by > 40% → PIP warning.
Month 5: 75% of quota. Miss by > 30% → formal PIP.
Month 6+: 100% of quota. Consistent miss → exit.
```
**PIP (Performance Improvement Plan) — not for show:**
- Should include specific, measurable targets (not "improve attitude")
- 30-60 day timeline
- Weekly check-ins with manager
- If targets aren't met: exit, no extensions
- A PIP that doesn't lead to improvement or exit is a management failure
**Rule:** Low performers who stay cost you your top performers. They watch what you tolerate.
FILE:cro-advisor/scripts/churn_analyzer.py
#!/usr/bin/env python3
"""
Churn & Retention Analyzer
===========================
Customer-level churn and Net Revenue Retention (NRR) analysis for B2B SaaS.
Calculates:
- Gross Revenue Retention (GRR) and Net Revenue Retention (NRR)
- Monthly and annual churn rates (logo + revenue)
- Cohort-based retention curves
- At-risk account identification
- Expansion revenue segmentation
- ARR waterfall (new / expansion / contraction / churn)
Usage:
python churn_analyzer.py
python churn_analyzer.py --csv customers.csv
python churn_analyzer.py --period 2026-Q1 --output summary
Input format (CSV):
customer_id, name, segment, arr, start_date, [churn_date], [expansion_arr], [contraction_arr]
Stdlib only. No dependencies.
"""
import csv
import sys
import json
import argparse
import statistics
from datetime import date, datetime, timedelta
from collections import defaultdict
from io import StringIO
from itertools import groupby
# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------
class Customer:
def __init__(self, customer_id, name, segment, arr, start_date,
churn_date=None, expansion_arr=0.0, contraction_arr=0.0,
health_score=None):
self.customer_id = customer_id
self.name = name
self.segment = segment
self.arr = float(arr)
self.start_date = self._parse_date(start_date)
self.churn_date = self._parse_date(churn_date) if churn_date else None
self.expansion_arr = float(expansion_arr or 0)
self.contraction_arr = float(contraction_arr or 0)
self.health_score = float(health_score) if health_score else None
@staticmethod
def _parse_date(value):
if not value or str(value).strip() in ("", "None", "null"):
return None
for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y", "%Y/%m/%d"):
try:
return datetime.strptime(str(value).strip(), fmt).date()
except ValueError:
continue
raise ValueError(f"Cannot parse date: {value!r}")
def is_churned(self):
return self.churn_date is not None
def is_active(self, as_of=None):
as_of = as_of or date.today()
if self.churn_date and self.churn_date <= as_of:
return False
return self.start_date <= as_of
def tenure_days(self, as_of=None):
as_of = as_of or date.today()
end = self.churn_date if self.churn_date else as_of
return (end - self.start_date).days
def tenure_months(self, as_of=None):
return self.tenure_days(as_of) / 30.44
def cohort_month(self):
"""Acquisition cohort: YYYY-MM of start_date."""
return self.start_date.strftime("%Y-%m")
def cohort_quarter(self):
q = (self.start_date.month - 1) // 3 + 1
return f"Q{q} {self.start_date.year}"
def net_arr(self):
"""Current ARR + expansion - contraction."""
return self.arr + self.expansion_arr - self.contraction_arr
def days_since_acquisition(self, as_of=None):
as_of = as_of or date.today()
return (as_of - self.start_date).days
# ---------------------------------------------------------------------------
# Core metrics
# ---------------------------------------------------------------------------
class RetentionAnalyzer:
def __init__(self, customers, as_of=None):
self.customers = customers
self.as_of = as_of or date.today()
def active_customers(self, as_of=None):
as_of = as_of or self.as_of
return [c for c in self.customers if c.is_active(as_of)]
def churned_customers(self, start=None, end=None):
"""Customers who churned in [start, end]."""
result = []
for c in self.customers:
if not c.churn_date:
continue
if start and c.churn_date < start:
continue
if end and c.churn_date > end:
continue
result.append(c)
return result
def arr_waterfall(self, period_start, period_end):
"""
Calculate ARR waterfall for a given period.
Returns dict with opening_arr, new_arr, expansion_arr, contraction_arr,
churned_arr, closing_arr, nrr, grr.
"""
# Opening: active at period start
opening_customers = [c for c in self.customers if c.is_active(period_start)]
opening_arr = sum(c.arr for c in opening_customers)
opening_ids = {c.customer_id for c in opening_customers}
# New: started during the period
new_customers = [
c for c in self.customers
if period_start < c.start_date <= period_end
]
new_arr = sum(c.arr for c in new_customers)
# Churned: were active at start, churn_date within period
churned = [
c for c in opening_customers
if c.churn_date and period_start < c.churn_date <= period_end
]
churned_arr = sum(c.arr for c in churned)
# Expansion and contraction: from customers active at opening
expansion = sum(
c.expansion_arr for c in opening_customers
if not c.is_churned() or (c.churn_date and c.churn_date > period_end)
)
contraction = sum(
c.contraction_arr for c in opening_customers
if not c.is_churned() or (c.churn_date and c.churn_date > period_end)
)
closing_arr = opening_arr + new_arr + expansion - contraction - churned_arr
grr = (opening_arr - contraction - churned_arr) / opening_arr if opening_arr else 0
nrr = (opening_arr + expansion - contraction - churned_arr) / opening_arr if opening_arr else 0
return {
"period_start": period_start.isoformat(),
"period_end": period_end.isoformat(),
"opening_arr": opening_arr,
"new_arr": new_arr,
"expansion_arr": expansion,
"contraction_arr": contraction,
"churned_arr": churned_arr,
"closing_arr": closing_arr,
"net_new_arr": new_arr + expansion - contraction - churned_arr,
"grr": max(0.0, grr),
"nrr": max(0.0, nrr),
}
def logo_churn_rate(self, period_start, period_end):
"""Logo churn rate for a period."""
opening = [c for c in self.customers if c.is_active(period_start)]
churned = [
c for c in opening
if c.churn_date and period_start < c.churn_date <= period_end
]
return len(churned) / len(opening) if opening else 0.0
def revenue_churn_rate(self, period_start, period_end):
"""Gross revenue churn rate for a period."""
opening = [c for c in self.customers if c.is_active(period_start)]
opening_arr = sum(c.arr for c in opening)
churned_arr = sum(
c.arr for c in opening
if c.churn_date and period_start < c.churn_date <= period_end
)
contraction = sum(c.contraction_arr for c in opening)
return (churned_arr + contraction) / opening_arr if opening_arr else 0.0
# ---------------------------------------------------------------------------
# Cohort analysis
# ---------------------------------------------------------------------------
class CohortAnalyzer:
def __init__(self, customers):
self.customers = customers
def build_cohorts(self):
"""Group customers by acquisition cohort (month)."""
cohorts = defaultdict(list)
for c in self.customers:
cohorts[c.cohort_month()].append(c)
return dict(sorted(cohorts.items()))
def retention_at_month(self, cohort_customers, months_after):
"""
What fraction of cohort ARR remains `months_after` months after acquisition?
"""
if not cohort_customers:
return None
opening_arr = sum(c.arr for c in cohort_customers)
if opening_arr == 0:
return None
earliest_start = min(c.start_date for c in cohort_customers)
check_date = earliest_start + timedelta(days=int(months_after * 30.44))
if check_date > date.today():
return None # Future — no data
retained_arr = sum(
c.arr for c in cohort_customers
if c.is_active(check_date)
)
return retained_arr / opening_arr
def retention_curve(self, cohort_customers, max_months=24):
"""Return retention at months 0, 3, 6, 9, 12, 18, 24."""
checkpoints = [0, 3, 6, 9, 12, 18, 24]
checkpoints = [m for m in checkpoints if m <= max_months]
curve = {}
for m in checkpoints:
rate = self.retention_at_month(cohort_customers, m)
if rate is not None:
curve[m] = rate
return curve
def cohort_report(self):
"""Returns dict: cohort → {size, opening_arr, retention_curve}."""
cohorts = self.build_cohorts()
report = {}
for cohort_month, customers in cohorts.items():
curve = self.retention_curve(customers)
report[cohort_month] = {
"customer_count": len(customers),
"opening_arr": sum(c.arr for c in customers),
"churned_count": sum(1 for c in customers if c.is_churned()),
"current_retention": curve.get(12, curve.get(max(curve.keys()) if curve else 0)),
"retention_curve": curve,
}
return report
def identify_at_risk(self, tenure_months_max=6, health_threshold=60):
"""
Identify at-risk customers based on:
- Low health score (if available)
- Short tenure (haven't proved long-term value)
- High contraction signals
"""
at_risk = []
for c in self.customers:
if c.is_churned():
continue
reasons = []
score = 0
# Health score signal
if c.health_score is not None and c.health_score < health_threshold:
reasons.append(f"Health score {c.health_score:.0f} < {health_threshold}")
score += 40
# Early tenure risk
tenure = c.tenure_months()
if tenure < tenure_months_max:
reasons.append(f"Tenure {tenure:.1f} months (< {tenure_months_max})")
score += 20
# Contraction signal
if c.contraction_arr > 0:
contraction_pct = c.contraction_arr / c.arr
reasons.append(f"Contraction {contraction_pct:.0%} of ARR")
score += 30
# No expansion in mature account
if tenure > 12 and c.expansion_arr == 0:
reasons.append("No expansion after 12+ months (stagnant)")
score += 10
if score > 0:
at_risk.append({
"customer_id": c.customer_id,
"name": c.name,
"segment": c.segment,
"arr": c.arr,
"tenure_months": round(tenure, 1),
"health_score": c.health_score,
"risk_score": score,
"risk_reasons": reasons,
})
return sorted(at_risk, key=lambda x: -x["risk_score"])
# ---------------------------------------------------------------------------
# Expansion analysis
# ---------------------------------------------------------------------------
class ExpansionAnalyzer:
def __init__(self, customers):
self.customers = customers
def expansion_summary(self):
active = [c for c in self.customers if not c.is_churned()]
expanding = [c for c in active if c.expansion_arr > 0]
contracting = [c for c in active if c.contraction_arr > 0]
total_arr = sum(c.arr for c in active)
total_expansion = sum(c.expansion_arr for c in active)
total_contraction = sum(c.contraction_arr for c in active)
return {
"active_customers": len(active),
"total_arr": total_arr,
"expanding_count": len(expanding),
"contracting_count": len(contracting),
"expansion_arr": total_expansion,
"contraction_arr": total_contraction,
"expansion_rate": total_expansion / total_arr if total_arr else 0,
"contraction_rate": total_contraction / total_arr if total_arr else 0,
"net_expansion_rate": (total_expansion - total_contraction) / total_arr if total_arr else 0,
}
def expansion_by_segment(self):
active = [c for c in self.customers if not c.is_churned()]
by_segment = defaultdict(lambda: {"arr": 0.0, "expansion": 0.0,
"contraction": 0.0, "count": 0})
for c in active:
seg = c.segment or "Unspecified"
by_segment[seg]["arr"] += c.arr
by_segment[seg]["expansion"] += c.expansion_arr
by_segment[seg]["contraction"] += c.contraction_arr
by_segment[seg]["count"] += 1
result = {}
for seg, data in by_segment.items():
arr = data["arr"]
result[seg] = {
"customer_count": data["count"],
"arr": arr,
"expansion_arr": data["expansion"],
"contraction_arr": data["contraction"],
"expansion_rate": data["expansion"] / arr if arr else 0,
"net_nrr_contribution": (arr + data["expansion"] - data["contraction"]) / arr if arr else 0,
}
return result
def top_expansion_candidates(self, min_tenure_months=6, min_arr=5000):
"""
Customers who are active, healthy tenure, but have zero expansion.
These are upsell/expansion targets.
"""
active = [c for c in self.customers if not c.is_churned()]
candidates = []
for c in active:
tenure = c.tenure_months()
if (tenure >= min_tenure_months
and c.arr >= min_arr
and c.expansion_arr == 0
and (c.health_score is None or c.health_score >= 60)):
candidates.append({
"customer_id": c.customer_id,
"name": c.name,
"segment": c.segment,
"arr": c.arr,
"tenure_months": round(tenure, 1),
"health_score": c.health_score,
})
return sorted(candidates, key=lambda x: -x["arr"])
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt_currency(value):
if value >= 1_000_000:
return f".2fM"
if value >= 1_000:
return f".1fK"
return f".0f"
def fmt_pct(value):
return f"{value * 100:.1f}%"
def nrr_status(nrr):
if nrr >= 1.20:
return "✅ World-class"
if nrr >= 1.10:
return "✅ Healthy"
if nrr >= 1.00:
return "⚠️ Acceptable"
if nrr >= 0.90:
return "🔴 Concerning"
return "🔴 Crisis"
def grr_status(grr):
if grr >= 0.90:
return "✅ Strong"
if grr >= 0.85:
return "⚠️ Acceptable"
return "🔴 Below threshold"
def print_header(title):
width = 70
print()
print("=" * width)
print(f" {title}")
print("=" * width)
def print_section(title):
print(f"\n--- {title} ---")
def print_full_report(customers, period_start, period_end):
analyzer = RetentionAnalyzer(customers, as_of=period_end)
cohort_analyzer = CohortAnalyzer(customers)
expansion_analyzer = ExpansionAnalyzer(customers)
print_header("CHURN & RETENTION ANALYZER")
print(f" Analysis period: {period_start.isoformat()} → {period_end.isoformat()}")
print(f" Total customers in dataset: {len(customers)}")
active = analyzer.active_customers(period_end)
churned_in_period = analyzer.churned_customers(period_start, period_end)
print(f" Active at period end: {len(active)}")
print(f" Churned in period: {len(churned_in_period)}")
# ── ARR Waterfall
print_section("ARR WATERFALL")
wf = analyzer.arr_waterfall(period_start, period_end)
print(f" Opening ARR: {fmt_currency(wf['opening_arr'])}")
print(f" + New Logo ARR: +{fmt_currency(wf['new_arr'])}")
print(f" + Expansion ARR: +{fmt_currency(wf['expansion_arr'])}")
print(f" - Contraction ARR: -{fmt_currency(wf['contraction_arr'])}")
print(f" - Churned ARR: -{fmt_currency(wf['churned_arr'])}")
print(f" {'─'*42}")
print(f" Closing ARR: {fmt_currency(wf['closing_arr'])}")
print(f" Net New ARR: {'+' if wf['net_new_arr'] >= 0 else ''}{fmt_currency(wf['net_new_arr'])}")
# ── NRR / GRR
print_section("RETENTION METRICS")
nrr = wf["nrr"]
grr = wf["grr"]
logo_churn = analyzer.logo_churn_rate(period_start, period_end)
rev_churn = analyzer.revenue_churn_rate(period_start, period_end)
print(f" NRR (Net Revenue Retention): {fmt_pct(nrr)} {nrr_status(nrr)}")
print(f" GRR (Gross Revenue Retention): {fmt_pct(grr)} {grr_status(grr)}")
print(f" Logo Churn Rate (period): {fmt_pct(logo_churn)}")
print(f" Revenue Churn Rate (period): {fmt_pct(rev_churn)}")
if wf["opening_arr"] > 0:
expansion_rate = wf["expansion_arr"] / wf["opening_arr"]
print(f" Expansion Rate (period): {fmt_pct(expansion_rate)}")
print()
print(f" NRR Benchmark: >120% world-class | 100-120% healthy | <100% fix immediately")
# ── Expansion summary
print_section("EXPANSION REVENUE")
exp = expansion_analyzer.expansion_summary()
print(f" Expanding customers: {exp['expanding_count']} / {exp['active_customers']} ({fmt_pct(exp['expanding_count']/exp['active_customers']) if exp['active_customers'] else '—'})")
print(f" Contracting: {exp['contracting_count']} / {exp['active_customers']}")
print(f" Expansion ARR: {fmt_currency(exp['expansion_arr'])} ({fmt_pct(exp['expansion_rate'])} of base)")
print(f" Contraction ARR: {fmt_currency(exp['contraction_arr'])}")
print(f" Net Expansion Rate: {fmt_pct(exp['net_expansion_rate'])}")
# ── Segment breakdown
print_section("SEGMENT BREAKDOWN (NRR Components)")
seg_data = expansion_analyzer.expansion_by_segment()
col_w = [18, 8, 12, 10, 10, 10]
h = (f" {'Segment':<{col_w[0]}} {'Custs':>{col_w[1]}} {'ARR':>{col_w[2]}} "
f"{'Expansion':>{col_w[3]}} {'Contraction':>{col_w[4]}} {'NRR':>{col_w[5]}}")
print(h)
print(" " + "-" * (sum(col_w) + 5))
for seg, data in sorted(seg_data.items(), key=lambda x: -x[1]["arr"]):
print(f" {seg:<{col_w[0]}} {data['customer_count']:>{col_w[1]}} "
f"{fmt_currency(data['arr']):>{col_w[2]}} "
f"{fmt_currency(data['expansion_arr']):>{col_w[3]}} "
f"{fmt_currency(data['contraction_arr']):>{col_w[4]}} "
f"{fmt_pct(data['net_nrr_contribution']):>{col_w[5]}}")
# ── Cohort retention
print_section("COHORT RETENTION CURVES")
cohort_report = cohort_analyzer.cohort_report()
print(f" {'Cohort':<10} {'Custs':>6} {'Opening ARR':>13} {'Mo.3':>8} {'Mo.6':>8} {'Mo.12':>8}")
print(" " + "-" * 57)
for cohort, data in cohort_report.items():
curve = data["retention_curve"]
m3 = fmt_pct(curve[3]) if 3 in curve else " —"
m6 = fmt_pct(curve[6]) if 6 in curve else " —"
m12 = fmt_pct(curve[12]) if 12 in curve else " —"
print(f" {cohort:<10} {data['customer_count']:>6} "
f"{fmt_currency(data['opening_arr']):>13} "
f"{m3:>8} {m6:>8} {m12:>8}")
# ── At-risk accounts
print_section("AT-RISK ACCOUNTS")
at_risk = cohort_analyzer.identify_at_risk()
if at_risk:
print(f" {'Customer':<22} {'Segment':<14} {'ARR':>10} {'Tenure':>8} {'Risk':>6} Reason")
print(" " + "-" * 80)
for acct in at_risk[:10]: # Top 10
reason_short = acct["risk_reasons"][0] if acct["risk_reasons"] else ""
tenure_str = f"{acct['tenure_months']}mo"
print(f" {acct['name']:<22} {acct['segment']:<14} "
f"{fmt_currency(acct['arr']):>10} {tenure_str:>8} "
f"{acct['risk_score']:>5} {reason_short}")
if len(at_risk) > 10:
print(f" ... and {len(at_risk) - 10} more at-risk accounts")
else:
print(" ✅ No at-risk accounts identified")
# ── Expansion candidates
print_section("EXPANSION CANDIDATES (no expansion yet, healthy tenure)")
candidates = expansion_analyzer.top_expansion_candidates()
if candidates:
print(f" {'Customer':<22} {'Segment':<14} {'ARR':>10} {'Tenure':>8} Action")
print(" " + "-" * 70)
for c in candidates[:8]:
action = "Upsell review" if c["arr"] > 20000 else "Seat expansion call"
tenure_str = f"{c['tenure_months']}mo"
print(f" {c['name']:<22} {c['segment']:<14} "
f"{fmt_currency(c['arr']):>10} {tenure_str:>8} {action}")
else:
print(" ✅ All eligible accounts have expansion in motion")
# ── Red flags
print_section("HEALTH FLAGS")
flags = []
if nrr < 1.0:
flags.append("🔴 NRR below 100% — revenue base is shrinking. Fix before scaling sales.")
if grr < 0.85:
flags.append(f"🔴 GRR {fmt_pct(grr)} — gross retention below 85% threshold. Churn is a product/CS problem.")
if logo_churn > 0.05:
flags.append(f"⚠️ Logo churn {fmt_pct(logo_churn)} this period — run cohort analysis to find the pattern.")
if exp["expansion_rate"] < 0.10 and exp["active_customers"] > 10:
flags.append("⚠️ Expansion rate below 10% — upsell motion is weak or non-existent.")
churned_arr_pct = wf["churned_arr"] / wf["opening_arr"] if wf["opening_arr"] else 0
if churned_arr_pct > 0.10:
flags.append(f"🔴 Revenue churn at {fmt_pct(churned_arr_pct)} of opening ARR this period — high urgency.")
if len(at_risk) > len(active) * 0.20:
flags.append(f"⚠️ {len(at_risk)} of {len(active)} active accounts flagged at-risk ({fmt_pct(len(at_risk)/len(active) if active else 0)})")
if flags:
for f in flags:
print(f" {f}")
else:
print(" ✅ No critical health flags")
print()
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
SAMPLE_CSV = """customer_id,name,segment,arr,start_date,churn_date,expansion_arr,contraction_arr,health_score
C001,Acme Manufacturing,Enterprise,120000,2023-01-15,,45000,0,82
C002,TechStart Inc,Mid-Market,28000,2023-02-01,,8000,0,74
C003,Global Retail Co,Enterprise,250000,2023-01-05,,0,25000,45
C004,MedTech Solutions,Mid-Market,45000,2023-03-10,,15000,0,88
C005,FinServ Holdings,Enterprise,185000,2023-01-20,2023-09-15,0,0,
C006,StartupHub Network,SMB,12000,2023-04-01,,0,3000,55
C007,EduPlatform Inc,Mid-Market,32000,2023-02-15,,10000,0,91
C008,BioLab Analytics,Enterprise,95000,2023-01-10,,20000,0,78
C009,RegionalBank Corp,Enterprise,310000,2023-03-01,,75000,0,85
C010,CloudOps Systems,Mid-Market,38000,2023-05-01,2024-01-10,0,0,
C011,InsurTech Platform,Mid-Market,55000,2023-06-15,,0,0,62
C012,LegalAI Corp,SMB,18000,2023-07-01,,5000,0,79
C013,RetailChain Ltd,Enterprise,140000,2023-04-20,,0,20000,41
C014,DataPipeline Co,Mid-Market,42000,2023-08-01,,12000,0,83
C015,NanoTech Startup,SMB,9500,2023-09-15,2024-02-28,0,0,
C016,MedDevice Corp,Enterprise,220000,2023-02-28,,60000,0,92
C017,ConsultingFirm XYZ,SMB,15000,2023-10-01,,0,5000,38
C018,GovTech Solutions,Enterprise,175000,2023-11-15,,0,0,71
C019,AgriData Systems,Mid-Market,31000,2024-01-10,,8000,0,77
C020,HealthcarePlus,Mid-Market,62000,2024-02-01,,0,0,65
"""
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def load_customers_from_csv(csv_text):
reader = csv.DictReader(StringIO(csv_text))
customers = []
errors = []
for i, row in enumerate(reader, start=2):
try:
c = Customer(
customer_id=row.get("customer_id", f"row_{i}"),
name=row.get("name", f"Customer {i}"),
segment=row.get("segment", ""),
arr=row.get("arr", 0),
start_date=row.get("start_date", ""),
churn_date=row.get("churn_date", None) or None,
expansion_arr=row.get("expansion_arr", 0) or 0,
contraction_arr=row.get("contraction_arr", 0) or 0,
health_score=row.get("health_score", None) or None,
)
customers.append(c)
except (ValueError, KeyError) as e:
errors.append(f" Row {i}: {e}")
if errors:
print("⚠️ Skipped rows with errors:")
for err in errors:
print(err)
return customers
def parse_period(period_str):
"""Parse 'YYYY-QN' or 'YYYY-MM' into (start_date, end_date)."""
if not period_str:
today = date.today()
q = (today.month - 1) // 3
start = date(today.year, q * 3 + 1, 1)
# End of current quarter
end_month = start.month + 2
end_year = start.year + (end_month - 1) // 12
end_month = ((end_month - 1) % 12) + 1
import calendar
end_day = calendar.monthrange(end_year, end_month)[1]
return start, date(end_year, end_month, end_day)
import calendar
if "-Q" in period_str:
year, qpart = period_str.split("-Q")
year = int(year)
q = int(qpart)
start_month = (q - 1) * 3 + 1
end_month = start_month + 2
start = date(year, start_month, 1)
end = date(year, end_month, calendar.monthrange(year, end_month)[1])
return start, end
# YYYY-MM
year, month = period_str.split("-")
year, month = int(year), int(month)
start = date(year, month, 1)
end = date(year, month, calendar.monthrange(year, month)[1])
return start, end
def main():
parser = argparse.ArgumentParser(
description="Churn & Retention Analyzer — NRR, cohort analysis, at-risk detection"
)
parser.add_argument(
"--csv", metavar="FILE",
help="CSV file with customer data (uses sample data if not provided)"
)
parser.add_argument(
"--period", metavar="PERIOD",
help='Analysis period: "2026-Q1" or "2026-03" (defaults to current quarter)'
)
parser.add_argument(
"--output", choices=["summary", "full", "json"],
default="full",
help="Output format (default: full)"
)
args = parser.parse_args()
# Load data
if args.csv:
try:
with open(args.csv, "r", encoding="utf-8") as f:
csv_text = f.read()
except FileNotFoundError:
print(f"Error: File not found: {args.csv}", file=sys.stderr)
sys.exit(1)
else:
print("No --csv provided. Using sample customer data.\n")
csv_text = SAMPLE_CSV
customers = load_customers_from_csv(csv_text)
if not customers:
print("No customers loaded. Exiting.", file=sys.stderr)
sys.exit(1)
period_start, period_end = parse_period(args.period)
if args.output == "json":
analyzer = RetentionAnalyzer(customers, as_of=period_end)
cohort_analyzer = CohortAnalyzer(customers)
expansion_analyzer = ExpansionAnalyzer(customers)
wf = analyzer.arr_waterfall(period_start, period_end)
output = {
"period": {"start": period_start.isoformat(), "end": period_end.isoformat()},
"arr_waterfall": wf,
"logo_churn_rate": analyzer.logo_churn_rate(period_start, period_end),
"revenue_churn_rate": analyzer.revenue_churn_rate(period_start, period_end),
"cohort_report": {k: {**v, "retention_curve": {str(m): r for m, r in v["retention_curve"].items()}}
for k, v in cohort_analyzer.cohort_report().items()},
"at_risk_accounts": cohort_analyzer.identify_at_risk(),
"expansion_summary": expansion_analyzer.expansion_summary(),
"expansion_by_segment": expansion_analyzer.expansion_by_segment(),
"expansion_candidates": expansion_analyzer.top_expansion_candidates(),
}
print(json.dumps(output, indent=2))
elif args.output == "summary":
analyzer = RetentionAnalyzer(customers, as_of=period_end)
wf = analyzer.arr_waterfall(period_start, period_end)
print_header("NRR SUMMARY")
print(f" Period: {period_start.isoformat()} → {period_end.isoformat()}")
print(f" NRR: {fmt_pct(wf['nrr'])} {nrr_status(wf['nrr'])}")
print(f" GRR: {fmt_pct(wf['grr'])} {grr_status(wf['grr'])}")
print(f" Opening: {fmt_currency(wf['opening_arr'])}")
print(f" Closing: {fmt_currency(wf['closing_arr'])}")
print(f" Net New: {fmt_currency(wf['net_new_arr'])}")
print()
else:
print_full_report(customers, period_start, period_end)
if __name__ == "__main__":
main()
FILE:cro-advisor/scripts/revenue_forecast_model.py
#!/usr/bin/env python3
"""
Revenue Forecast Model
======================
Pipeline-based revenue forecasting for B2B SaaS.
Models:
- Weighted pipeline (stage probability × deal value)
- Historical win rate adjustment (calibrate to actuals)
- Scenario analysis (conservative / base / upside)
- Monthly and quarterly projection with confidence ranges
Usage:
python revenue_forecast_model.py
python revenue_forecast_model.py --csv pipeline.csv
python revenue_forecast_model.py --scenario conservative
Input format (CSV):
deal_id, name, stage, arr_value, close_date, rep, segment
Stdlib only. No dependencies.
"""
import csv
import sys
import json
import argparse
import statistics
from datetime import date, datetime, timedelta
from collections import defaultdict
from io import StringIO
# ---------------------------------------------------------------------------
# Stage configuration
# ---------------------------------------------------------------------------
DEFAULT_STAGE_PROBABILITIES = {
"discovery": 0.10,
"qualification": 0.25,
"demo": 0.40,
"proposal": 0.55,
"poc": 0.65,
"negotiation": 0.80,
"verbal_commit": 0.92,
"closed_won": 1.00,
"closed_lost": 0.00,
}
SCENARIO_MULTIPLIERS = {
"conservative": 0.85, # Win rate 15% below historical
"base": 1.00, # Historical win rate
"upside": 1.15, # Win rate 15% above historical
}
# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------
class Deal:
def __init__(self, deal_id, name, stage, arr_value, close_date, rep="", segment=""):
self.deal_id = deal_id
self.name = name
self.stage = stage.lower().replace(" ", "_").replace("/", "_")
self.arr_value = float(arr_value)
self.close_date = self._parse_date(close_date)
self.rep = rep
self.segment = segment
@staticmethod
def _parse_date(value):
for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y", "%Y/%m/%d"):
try:
return datetime.strptime(str(value), fmt).date()
except ValueError:
continue
raise ValueError(f"Cannot parse date: {value!r}")
@property
def quarter(self):
q = (self.close_date.month - 1) // 3 + 1
return f"Q{q} {self.close_date.year}"
@property
def month_key(self):
return self.close_date.strftime("%Y-%m")
def weighted_value(self, stage_probs, scenario="base"):
prob = stage_probs.get(self.stage, 0.0)
multiplier = SCENARIO_MULTIPLIERS.get(scenario, 1.0)
# Clamp probability to [0, 1]
adjusted = min(1.0, max(0.0, prob * multiplier))
return self.arr_value * adjusted
def is_open(self):
return self.stage not in ("closed_won", "closed_lost")
def is_closed_won(self):
return self.stage == "closed_won"
# ---------------------------------------------------------------------------
# Win rate calibration
# ---------------------------------------------------------------------------
def calculate_historical_win_rates(deals):
"""
Calculate actual win rates per stage from closed deals.
Returns a dict: stage → win_rate (float).
Requires deals that were at each stage and are now closed won/lost.
"""
# In a real implementation, you'd have historical stage-at-point-in-time data.
# Here we approximate: among closed deals, what fraction were won?
closed = [d for d in deals if not d.is_open()]
if not closed:
return {}
won = [d for d in closed if d.is_closed_won()]
overall_rate = len(won) / len(closed) if closed else 0.0
# Stage-level calibration: adjust default probs by actual overall rate
# (In production: use CRM historical stage-level conversion data)
calibrated = {}
for stage, default_prob in DEFAULT_STAGE_PROBABILITIES.items():
if overall_rate > 0:
calibrated[stage] = min(1.0, default_prob * (overall_rate / 0.25))
else:
calibrated[stage] = default_prob
return calibrated
# ---------------------------------------------------------------------------
# Forecast engine
# ---------------------------------------------------------------------------
class ForecastEngine:
def __init__(self, deals, stage_probs=None):
self.deals = deals
self.stage_probs = stage_probs or DEFAULT_STAGE_PROBABILITIES
def open_deals(self):
return [d for d in self.deals if d.is_open()]
def closed_won_deals(self):
return [d for d in self.deals if d.is_closed_won()]
def pipeline_by_month(self, scenario="base"):
"""Returns dict: month_key → weighted ARR."""
result = defaultdict(float)
for deal in self.open_deals():
result[deal.month_key] += deal.weighted_value(self.stage_probs, scenario)
return dict(sorted(result.items()))
def pipeline_by_quarter(self, scenario="base"):
"""Returns dict: quarter → weighted ARR."""
result = defaultdict(float)
for deal in self.open_deals():
result[deal.quarter] += deal.weighted_value(self.stage_probs, scenario)
return dict(sorted(result.items()))
def coverage_ratio(self, quota, period_filter=None):
"""
Pipeline coverage = total pipeline ÷ quota.
period_filter: if set, only include deals with close_date in that period.
"""
pipeline = sum(
d.arr_value for d in self.open_deals()
if period_filter is None or d.quarter == period_filter
)
return pipeline / quota if quota else 0.0
def scenario_summary(self, periods=None):
"""
Returns dict: period → {conservative, base, upside, open_pipeline}.
periods: list of month_keys to include; if None, all months.
"""
summaries = {}
all_months = sorted(set(d.month_key for d in self.open_deals()))
target_months = periods or all_months
for month in target_months:
deals_in_month = [d for d in self.open_deals() if d.month_key == month]
if not deals_in_month:
continue
summaries[month] = {
"deal_count": len(deals_in_month),
"open_pipeline": sum(d.arr_value for d in deals_in_month),
"conservative": sum(d.weighted_value(self.stage_probs, "conservative") for d in deals_in_month),
"base": sum(d.weighted_value(self.stage_probs, "base") for d in deals_in_month),
"upside": sum(d.weighted_value(self.stage_probs, "upside") for d in deals_in_month),
}
return summaries
def rep_performance(self):
"""Returns dict: rep → {pipeline, weighted_base, deal_count, avg_deal_size}."""
rep_data = defaultdict(lambda: {"pipeline": 0.0, "weighted_base": 0.0,
"deal_count": 0, "deals": []})
for deal in self.open_deals():
rep_data[deal.rep]["pipeline"] += deal.arr_value
rep_data[deal.rep]["weighted_base"] += deal.weighted_value(self.stage_probs, "base")
rep_data[deal.rep]["deal_count"] += 1
rep_data[deal.rep]["deals"].append(deal.arr_value)
result = {}
for rep, data in rep_data.items():
deals = data["deals"]
result[rep] = {
"pipeline": data["pipeline"],
"weighted_base": data["weighted_base"],
"deal_count": data["deal_count"],
"avg_deal_size": statistics.mean(deals) if deals else 0.0,
}
return result
def segment_breakdown(self, scenario="base"):
"""Returns dict: segment → weighted ARR."""
result = defaultdict(float)
for deal in self.open_deals():
result[deal.segment or "unspecified"] += deal.weighted_value(self.stage_probs, scenario)
return dict(result)
def stage_distribution(self):
"""Returns dict: stage → {count, total_arr, avg_arr}."""
result = defaultdict(lambda: {"count": 0, "total_arr": 0.0})
for deal in self.open_deals():
result[deal.stage]["count"] += 1
result[deal.stage]["total_arr"] += deal.arr_value
out = {}
for stage, data in result.items():
out[stage] = {
"count": data["count"],
"total_arr": data["total_arr"],
"avg_arr": data["total_arr"] / data["count"] if data["count"] else 0,
"probability": self.stage_probs.get(stage, 0.0),
}
return out
def confidence_interval(self, scenario="base", iterations=1000):
"""
Monte Carlo simulation to generate confidence interval around base forecast.
Each deal wins/loses based on its probability; runs iterations times.
Returns (p10, p50, p90) of total expected ARR.
"""
import random
random.seed(42)
totals = []
for _ in range(iterations):
total = 0.0
for deal in self.open_deals():
prob = min(1.0, self.stage_probs.get(deal.stage, 0.0) * SCENARIO_MULTIPLIERS[scenario])
if random.random() < prob:
total += deal.arr_value
totals.append(total)
totals.sort()
n = len(totals)
return (
totals[int(n * 0.10)], # P10 (conservative)
totals[int(n * 0.50)], # P50 (median)
totals[int(n * 0.90)], # P90 (upside)
)
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def fmt_currency(value):
if value >= 1_000_000:
return f".2fM"
if value >= 1_000:
return f".1fK"
return f".0f"
def fmt_pct(value):
return f"{value * 100:.1f}%"
def print_header(title):
width = 70
print()
print("=" * width)
print(f" {title}")
print("=" * width)
def print_section(title):
print(f"\n--- {title} ---")
def print_report(engine, quota=None, current_quarter=None):
open_deals = engine.open_deals()
won_deals = engine.closed_won_deals()
print_header("REVENUE FORECAST MODEL")
print(f" Generated: {date.today().isoformat()}")
print(f" Open deals: {len(open_deals)}")
print(f" Closed Won (in dataset): {len(won_deals)}")
total_pipeline = sum(d.arr_value for d in open_deals)
total_won = sum(d.arr_value for d in won_deals)
print(f" Total open pipeline: {fmt_currency(total_pipeline)}")
print(f" Total closed won: {fmt_currency(total_won)}")
# ── Coverage ratio
if quota:
print_section("PIPELINE COVERAGE")
q = current_quarter or "this quarter"
ratio = engine.coverage_ratio(quota, period_filter=current_quarter)
status = "✅ Healthy" if ratio >= 3.0 else ("⚠️ Thin" if ratio >= 2.0 else "🔴 Critical")
print(f" Quota target: {fmt_currency(quota)}")
print(f" Coverage ratio: {ratio:.1f}x {status}")
print(f" (Minimum healthy = 3x; < 2x = pipeline emergency)")
# ── Stage distribution
print_section("STAGE DISTRIBUTION")
stage_dist = engine.stage_distribution()
col_w = [28, 8, 14, 12, 10]
header = f" {'Stage':<{col_w[0]}} {'Deals':>{col_w[1]}} {'Pipeline':>{col_w[2]}} {'Avg Size':>{col_w[3]}} {'Win Prob':>{col_w[4]}}"
print(header)
print(" " + "-" * (sum(col_w) + 4))
for stage, data in sorted(stage_dist.items(), key=lambda x: -x[1]["total_arr"]):
print(f" {stage:<{col_w[0]}} {data['count']:>{col_w[1]}} "
f"{fmt_currency(data['total_arr']):>{col_w[2]}} "
f"{fmt_currency(data['avg_arr']):>{col_w[3]}} "
f"{fmt_pct(data['probability']):>{col_w[4]}}")
# ── Scenario forecast by month
print_section("MONTHLY FORECAST — ALL SCENARIOS")
summaries = engine.scenario_summary()
col_w2 = [10, 8, 14, 14, 14, 14]
h2 = (f" {'Month':<{col_w2[0]}} {'Deals':>{col_w2[1]}} "
f"{'Pipeline':>{col_w2[2]}} {'Conservative':>{col_w2[3]}} "
f"{'Base':>{col_w2[4]}} {'Upside':>{col_w2[5]}}")
print(h2)
print(" " + "-" * (sum(col_w2) + 5))
for month, data in summaries.items():
print(f" {month:<{col_w2[0]}} {data['deal_count']:>{col_w2[1]}} "
f"{fmt_currency(data['open_pipeline']):>{col_w2[2]}} "
f"{fmt_currency(data['conservative']):>{col_w2[3]}} "
f"{fmt_currency(data['base']):>{col_w2[4]}} "
f"{fmt_currency(data['upside']):>{col_w2[5]}}")
# ── Quarterly rollup
print_section("QUARTERLY FORECAST ROLLUP")
q_conservative = defaultdict(float)
q_base = defaultdict(float)
q_upside = defaultdict(float)
q_pipeline = defaultdict(float)
q_count = defaultdict(int)
for deal in open_deals:
q_conservative[deal.quarter] += deal.weighted_value(engine.stage_probs, "conservative")
q_base[deal.quarter] += deal.weighted_value(engine.stage_probs, "base")
q_upside[deal.quarter] += deal.weighted_value(engine.stage_probs, "upside")
q_pipeline[deal.quarter] += deal.arr_value
q_count[deal.quarter] += 1
quarters = sorted(q_base.keys())
col_w3 = [10, 8, 14, 14, 14, 14]
h3 = (f" {'Quarter':<{col_w3[0]}} {'Deals':>{col_w3[1]}} "
f"{'Pipeline':>{col_w3[2]}} {'Conservative':>{col_w3[3]}} "
f"{'Base':>{col_w3[4]}} {'Upside':>{col_w3[5]}}")
print(h3)
print(" " + "-" * (sum(col_w3) + 5))
for q in quarters:
print(f" {q:<{col_w3[0]}} {q_count[q]:>{col_w3[1]}} "
f"{fmt_currency(q_pipeline[q]):>{col_w3[2]}} "
f"{fmt_currency(q_conservative[q]):>{col_w3[3]}} "
f"{fmt_currency(q_base[q]):>{col_w3[4]}} "
f"{fmt_currency(q_upside[q]):>{col_w3[5]}}")
# ── Monte Carlo confidence interval
print_section("CONFIDENCE INTERVAL (Monte Carlo, 1,000 simulations)")
p10, p50, p90 = engine.confidence_interval("base")
print(f" P10 (conservative floor): {fmt_currency(p10)}")
print(f" P50 (median expected): {fmt_currency(p50)}")
print(f" P90 (upside ceiling): {fmt_currency(p90)}")
print(f" Range spread: {fmt_currency(p90 - p10)}")
# ── Rep performance
print_section("REP PIPELINE PERFORMANCE")
rep_perf = engine.rep_performance()
if rep_perf:
col_w4 = [20, 8, 14, 14, 12]
h4 = (f" {'Rep':<{col_w4[0]}} {'Deals':>{col_w4[1]}} "
f"{'Pipeline':>{col_w4[2]}} {'Weighted':>{col_w4[3]}} {'Avg Size':>{col_w4[4]}}")
print(h4)
print(" " + "-" * (sum(col_w4) + 4))
for rep, data in sorted(rep_perf.items(), key=lambda x: -x[1]["pipeline"]):
print(f" {rep:<{col_w4[0]}} {data['deal_count']:>{col_w4[1]}} "
f"{fmt_currency(data['pipeline']):>{col_w4[2]}} "
f"{fmt_currency(data['weighted_base']):>{col_w4[3]}} "
f"{fmt_currency(data['avg_deal_size']):>{col_w4[4]}}")
# ── Segment breakdown
print_section("SEGMENT BREAKDOWN (Base Forecast)")
seg = engine.segment_breakdown("base")
for segment, value in sorted(seg.items(), key=lambda x: -x[1]):
bar_len = int((value / total_pipeline) * 30) if total_pipeline else 0
bar = "█" * bar_len
print(f" {segment:<20} {fmt_currency(value):>12} {bar}")
# ── Red flags
print_section("FORECAST HEALTH FLAGS")
flags = []
if total_pipeline > 0:
coverage = total_pipeline / quota if quota else None
if coverage and coverage < 2.0:
flags.append("🔴 Pipeline coverage below 2x — serious shortfall risk this quarter")
elif coverage and coverage < 3.0:
flags.append("⚠️ Pipeline coverage below 3x — limited buffer for slippage")
# Stage concentration risk
early_stage_pct = sum(
d.arr_value for d in open_deals
if engine.stage_probs.get(d.stage, 0) < 0.30
) / total_pipeline
if early_stage_pct > 0.60:
flags.append(f"⚠️ {fmt_pct(early_stage_pct)} of pipeline in early stages (< 30% probability)")
# Deal concentration
deal_values = sorted([d.arr_value for d in open_deals], reverse=True)
if deal_values and deal_values[0] / total_pipeline > 0.25:
flags.append(f"⚠️ Top deal is {fmt_pct(deal_values[0]/total_pipeline)} of pipeline — concentration risk")
# Spread between scenarios
total_conservative = sum(d.weighted_value(engine.stage_probs, "conservative") for d in open_deals)
total_upside = sum(d.weighted_value(engine.stage_probs, "upside") for d in open_deals)
spread = (total_upside - total_conservative) / total_conservative if total_conservative else 0
if spread > 0.40:
flags.append(f"⚠️ High scenario spread ({fmt_pct(spread)}) — forecast confidence is low")
if flags:
for f in flags:
print(f" {f}")
else:
print(" ✅ No critical flags detected")
print()
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
SAMPLE_CSV = """deal_id,name,stage,arr_value,close_date,rep,segment
D001,Acme Corp ERP Integration,negotiation,85000,2026-03-15,Sarah Chen,Enterprise
D002,TechStart PLG Expansion,proposal,28000,2026-03-28,Marcus Webb,Mid-Market
D003,Global Retail Co,verbal_commit,220000,2026-03-10,Sarah Chen,Enterprise
D004,BioLab Analytics,poc,62000,2026-04-05,Jamie Park,Mid-Market
D005,FinServ Holdings,demo,150000,2026-04-20,Sarah Chen,Enterprise
D006,MidWest Logistics,qualification,35000,2026-04-30,Marcus Webb,Mid-Market
D007,Edu Platform Inc,negotiation,42000,2026-03-25,Jamie Park,SMB
D008,Healthcare Connect,proposal,95000,2026-05-15,Sarah Chen,Enterprise
D009,Startup Hub Network,demo,18000,2026-04-10,Marcus Webb,SMB
D010,CloudOps Systems,poc,75000,2026-05-01,Jamie Park,Mid-Market
D011,National Bank Corp,verbal_commit,310000,2026-03-31,Sarah Chen,Enterprise
D012,RetailTech Co,qualification,22000,2026-05-20,Marcus Webb,SMB
D013,InsurTech Platform,negotiation,88000,2026-04-15,Jamie Park,Mid-Market
D014,GovTech Solutions,proposal,175000,2026-06-01,Sarah Chen,Enterprise
D015,AgriData Systems,demo,31000,2026-05-10,Marcus Webb,Mid-Market
D016,Legal AI Corp,poc,55000,2026-04-25,Jamie Park,Mid-Market
D017,Closed Won Deal,closed_won,120000,2026-02-15,Sarah Chen,Enterprise
D018,Lost Deal,closed_lost,45000,2026-02-20,Marcus Webb,Mid-Market
"""
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def load_deals_from_csv(csv_text):
reader = csv.DictReader(StringIO(csv_text))
deals = []
errors = []
for i, row in enumerate(reader, start=2):
try:
deal = Deal(
deal_id=row.get("deal_id", f"row_{i}"),
name=row.get("name", ""),
stage=row.get("stage", ""),
arr_value=row.get("arr_value", 0),
close_date=row.get("close_date", ""),
rep=row.get("rep", ""),
segment=row.get("segment", ""),
)
deals.append(deal)
except (ValueError, KeyError) as e:
errors.append(f" Row {i}: {e}")
if errors:
print("⚠️ Skipped rows with errors:")
for err in errors:
print(err)
return deals
def main():
parser = argparse.ArgumentParser(
description="Revenue Forecast Model — pipeline-based ARR forecasting"
)
parser.add_argument(
"--csv", metavar="FILE",
help="CSV file with pipeline data (uses sample data if not provided)"
)
parser.add_argument(
"--quota", type=float, default=1_000_000,
help="Quarterly quota target in ARR (default: $1,000,000)"
)
parser.add_argument(
"--quarter", metavar="QUARTER",
help='Current quarter filter e.g. "Q2 2026" (optional)'
)
parser.add_argument(
"--scenario", choices=["conservative", "base", "upside"],
default="base",
help="Primary scenario to report (default: base)"
)
parser.add_argument(
"--json", action="store_true",
help="Output forecast as JSON instead of formatted report"
)
args = parser.parse_args()
# Load data
if args.csv:
try:
with open(args.csv, "r", encoding="utf-8") as f:
csv_text = f.read()
except FileNotFoundError:
print(f"Error: File not found: {args.csv}", file=sys.stderr)
sys.exit(1)
else:
print("No --csv provided. Using sample pipeline data.\n")
csv_text = SAMPLE_CSV
deals = load_deals_from_csv(csv_text)
if not deals:
print("No deals loaded. Exiting.", file=sys.stderr)
sys.exit(1)
# Calibrate win rates from closed deals
historical_probs = calculate_historical_win_rates(deals)
stage_probs = historical_probs if historical_probs else DEFAULT_STAGE_PROBABILITIES
engine = ForecastEngine(deals, stage_probs=stage_probs)
if args.json:
output = {
"generated": date.today().isoformat(),
"quota": args.quota,
"open_pipeline": sum(d.arr_value for d in engine.open_deals()),
"coverage_ratio": engine.coverage_ratio(args.quota, args.quarter),
"monthly_forecast": engine.scenario_summary(),
"quarterly_base": engine.pipeline_by_quarter("base"),
"confidence_interval": dict(zip(
["p10", "p50", "p90"],
engine.confidence_interval("base")
)),
"rep_performance": engine.rep_performance(),
"segment_breakdown": engine.segment_breakdown("base"),
}
print(json.dumps(output, indent=2))
else:
print_report(engine, quota=args.quota, current_quarter=args.quarter)
if __name__ == "__main__":
main()
FILE:cs-onboard/SKILL.md
---
name: "cs-onboard"
description: "Founder onboarding interview that captures company context across 7 dimensions. Invoke with /cs:setup for initial interview or /cs:update for quarterly refresh. Generates ~/.claude/company-context.md used by all C-suite advisor skills."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: orchestration
updated: 2026-03-05
frameworks: founder-interview, context-capture, quarterly-refresh
---
# C-Suite Onboarding
Structured founder interview that builds the company context file powering every C-suite advisor. One 45-minute conversation. Persistent context across all roles.
## Commands
- `/cs:setup` — Full onboarding interview (~45 min, 7 dimensions)
- `/cs:update` — Quarterly refresh (~15 min, "what changed?")
## Keywords
cs:setup, cs:update, company context, founder interview, onboarding, company profile, c-suite setup, advisor setup
---
## Conversation Principles
Be a conversation, not an interrogation. Ask one question at a time. Follow threads. Reflect back: "So the real issue sounds like X — is that right?" Watch for what they skip — that's where the real story lives. Never read a list of questions.
Open with: *"Tell me about the company in your own words — what are you building and why does it matter?"*
---
## 7 Interview Dimensions
### 1. Company Identity
Capture: what they do, who it's for, the real founding "why," one-sentence pitch, non-negotiable values.
Key probe: *"What's a value you'd fire someone over violating?"*
Red flag: Values that sound like marketing copy.
### 2. Stage & Scale
Capture: headcount (FT vs contractors), revenue range, runway, stage (pre-PMF / scaling / optimizing), what broke in last 90 days.
Key probe: *"If you had to label your stage — still finding PMF, scaling what works, or optimizing?"*
### 3. Founder Profile
Capture: self-identified superpower, acknowledged blind spots, archetype (product/sales/technical/operator), what actually keeps them up at night.
Key probe: *"What would your co-founder say you should stop doing?"*
Red flag: No blind spots, or weakness framed as a strength.
### 4. Team & Culture
Capture: team in 3 words, last real conflict and resolution, which values are real vs aspirational, strongest and weakest leader.
Key probe: *"Which of your stated values is most real? Which is a poster on the wall?"*
Red flag: "We have no conflict."
### 5. Market & Competition
Capture: who's winning and why (honest version), real unfair advantage, the one competitive move that could hurt them.
Key probe: *"What's your real unfair advantage — not the investor version?"*
Red flag: "We have no real competition."
### 6. Current Challenges
Capture: priority stack-rank across product/growth/people/money/operations, the decision they've been avoiding, the "one extra day" answer.
Key probe: *"What's the decision you've been putting off for weeks?"*
Note: The "extra day" answer reveals true priorities.
### 7. Goals & Ambition
Capture: 12-month target (specific), 36-month target (directional), exit vs build-forever orientation, personal success definition.
Key probe: *"What does success look like for you personally — separate from the company?"*
---
## Output: company-context.md
After the interview, generate `~/.claude/company-context.md` using `templates/company-context-template.md`.
Fill every section. Write `[not captured]` for unknowns — never leave blank. Add timestamp, mark as `fresh`.
Tell the founder: *"I've captured everything in your company context. Every advisor will use this to give specific, relevant advice. Run /cs:update in 90 days to keep it current."*
---
## /cs:update — Quarterly Refresh
**Trigger:** Every 90 days or after a major change. Duration: ~15 minutes.
Open with: *"It's been [X time] since we did your company context. What's changed?"*
Walk each dimension with one "what changed?" question:
1. Identity: same mission or shifted?
2. Scale: team, revenue, runway now?
3. Founder: role or what's stretching you?
4. Team: any leadership changes?
5. Market: any competitive surprises?
6. Challenges: #1 problem now vs 90 days ago?
7. Goals: still on track for 12-month target?
Update the context file, refresh timestamp, reset to `fresh`.
---
## Context File Location
`~/.claude/company-context.md` — single source of truth for all C-suite skills. Do not move it. Do not create duplicates.
## References
- `templates/company-context-template.md` — blank template for output
- `references/interview-guide.md` — deep interview craft: probes, red flags, handling reluctant founders
FILE:cs-onboard/references/interview-guide.md
# Interview Craft Guide
Deep operational guide for conducting the `/cs:setup` founder interview. Not a script — a thinking tool. Read before every interview. Internalize it, then put it away.
---
## The Core Problem
Most context-gathering fails because it captures what founders say, not what they mean. Founders are practiced storytellers. They have investor pitches, board narratives, team rallies. They tell good stories. Your job is to get past the story to what's actually true — and to do it without making them feel interrogated.
The best interview doesn't feel like an interview. It feels like a conversation with a smart advisor who gets it.
---
## Before You Start
Set the frame:
> "This isn't a quiz. There are no right answers. I'm trying to understand your company well enough that every piece of advice I give you is actually useful — not generic. The more honest you are, the more useful this gets. Nothing leaves this conversation."
Then shut up and let them talk.
---
## Reading the Room
Pay attention to:
- **Energy shifts.** Where do they speed up? What makes them lean in? That's what they care about. What makes them vague or flat? That's where the real issue lives.
- **What they lead with.** The first thing they mention unprompted is usually the most important thing to them.
- **Repetition.** If a topic comes up twice, it's significant. Three times and it's the real problem.
- **Hedging language.** "We're pretty much aligned on..." / "Things are mostly fine..." / "It's not really a problem yet..." — probe these. "Pretty much" is doing a lot of work there.
- **Skips.** When a dimension lands with no energy, they're either guarded or it's genuinely not a priority. Figure out which.
---
## Follow-Up Probe Library
### When the answer is vague
- "Can you give me a specific example?"
- "What does that look like on a Tuesday morning?"
- "If I asked your co-founder / direct report, what would they say?"
- "How would you know if that was actually true?"
### When the answer is suspiciously polished
- "That's the investor version — what's the version you'd tell your co-founder at 11pm?"
- "If that's true, what explains [specific contradicting data point]?"
- "What would a skeptic say about that?"
### When they skip something
- "You moved past [topic] quickly — is that because it's not a problem, or because it's too big to get into?"
- "Come back to [topic] — tell me more about that."
### When they say "everything is fine"
- "What's the thing that keeps you up at night even though you know you shouldn't worry about it?"
- "If something was going to surprise you in a bad way in the next 90 days, what would it be?"
- "What would your board member who's most worried about the company say?"
### When they're guarded
- Slow down. Don't push harder — push softer.
- "You don't have to share numbers if you're not comfortable — ranges are fine."
- Acknowledge the complexity: "This stuff is genuinely hard to talk about."
- Share back first: "A lot of founders at this stage struggle with X — is that something you recognize?"
### When they go long
Let them run for a bit. Then: "Let me make sure I captured what matters here — is it that [summary]?"
It helps you confirm understanding and signals you're tracking.
---
## Red Flag Patterns and What to Do
### "We have no real competition."
**Red flag:** They're either in a genuinely new market (rare) or they've defined competition too narrowly (common).
**Probe:** "What would someone do today if your product didn't exist? Who benefits if you fail?"
### "Our values are X, Y, Z."
**Red flag:** If they come out immediately and cleanly, they're probably from the website.
**Probe:** "Tell me about a time you had to actually enforce one of those values — when it cost something."
### "The team is great. Everyone's aligned."
**Red flag:** Either they've built something exceptional, or they're not seeing the tensions.
**Probe:** "What's the last thing you disagreed with someone on the team about? How did it go?"
### "I don't really have blind spots."
**Red flag:** Everyone has blind spots. Founders who can't name theirs are the most dangerous.
**Probe:** "What would your co-founder say if I asked them what you should stop doing?"
**Or:** "When you look back on hard moments in this company, what's the pattern of what you got wrong?"
### "Revenue is good, things are growing."
**Red flag:** "Good" is not a number.
**Probe:** "Give me a range — is this $100K ARR, $1M, $10M? I'm not sharing it anywhere."
### "We just need more customers."
**Red flag:** This is almost never the root problem.
**Probe:** "What's driving the growth you have? Why aren't more customers finding you, or converting, or staying?"
---
## Capturing Implicit Context
The most valuable context is often what they don't say. Document it.
**Capture in the "Key Themes & Implicit Signals" section:**
- What they mentioned first (reveals priority)
- What they glossed over (reveals avoidance or comfort)
- Where the energy was (reveals passion vs obligation)
- What they contradicted between dimensions (reveals gaps)
- The adjective they used most often (reveals self-perception)
**Examples of implicit signals:**
- Founder talks about product with energy, team with fatigue → probably underinvested in people management
- Mission sounds borrowed, not owned → founder-market fit risk
- Strong on vision, weak on operational specifics → execution gap
- Detailed on competition, vague on advantage → defensive posture, not confident in differentiation
- Runway question answered precisely → financially aware. Answered vaguely → either worried or detached.
---
## Handling Reluctant Founders
Some founders are guarded. Usually for one of three reasons:
1. **They don't trust you yet.** Give it time. Ask easier questions first. Build rapport.
2. **They're in denial.** Something is wrong and they're not ready to say it. Circles around topics, comes back to them.
3. **They're protecting someone.** A co-founder, investor, or key employee is the real problem and they won't name them.
**Tactics:**
- Give them an out: "You don't have to answer this specifically — just give me the shape of it."
- Normalize the problem: "A lot of founders at this stage are dealing with X..."
- Ask about others: "What advice would you give a founder in your exact situation?"
- Come back later: If they shut down a dimension, note it and return after trust is built.
---
## After the Interview
Before generating the file:
1. **Read back your notes.** Find the 3–5 most important things. They should be in the output.
2. **Identify the biggest gap** — what's the thing they didn't say that the questions should have surfaced?
3. **Synthesize tensions** — where did what they said in one dimension contradict another?
4. **Write the Watch List** — what needs to be re-checked in 90 days?
Then generate the context file. The last section — "Key Themes & Implicit Signals" — is the most important one. Don't skip it.
---
## Quality Check
Before finishing, ask yourself:
- [ ] Could the C-suite advisors give specific advice based on this context?
- [ ] Does this capture what's real vs what's aspirational?
- [ ] Is the Watch List honest about what's uncertain or worrying?
- [ ] Does the founder profile feel like a real person, not a LinkedIn bio?
- [ ] Did I capture implicit signals, not just explicit answers?
If any answer is no, go back and fill it in.
---
## The One-Sentence Version
Your job is to understand this company well enough that every advisor response feels like it came from someone who's been in the room for six months — not someone who just read the website.
FILE:cs-onboard/templates/company-context-template.md
# Company Context
**Last updated:** [DATE]
**Status:** fresh | stale (>90 days)
**Interview type:** full | update
---
## 1. Company Identity
**What we do:**
[One paragraph — product/service, who it's for, core use case]
**Why we exist (founding reason):**
[The real reason, not the pitch]
**One-sentence pitch:**
[Sharpened during interview]
**Non-negotiable values:**
- [Value 1] — [what would violate it]
- [Value 2] — [what would violate it]
- [Value 3] — [what would violate it]
---
## 2. Stage & Scale
**Team size:** [N full-time] + [N contractors/part-time]
**Revenue:** [ARR/MRR range, e.g., "$500K–$1M ARR"]
**Runway:** [N months]
**Stage:** pre-PMF | scaling | optimizing
**What broke recently (last 90 days):**
[Specific failure, cost, and root cause if known]
---
## 3. Founder Profile
**Name / Role:**
**Superpower:**
[What they do better than almost anyone on their team]
**Blind spots:**
[Acknowledged or revealed — be specific]
**Founder archetype:** product | sales | technical | operator
**What keeps them up at night:**
[The real concern, not the investor-safe version]
---
## 4. Team & Culture
**Team in 3 words:** [word], [word], [word]
**Culture — what's real:**
[Which values are actually lived]
**Culture — what's aspirational:**
[Which values are poster-on-the-wall]
**Strongest leader:**
[Role / what makes them strong]
**Weakest seat:**
[Role / what the risk is]
**Last significant conflict:**
[What happened, how it resolved, what it revealed]
---
## 5. Market & Competition
**Who's winning right now:**
[Market leader + honest reason why]
**Unfair advantage (honest version):**
[Not the pitch — the real structural edge]
**Kill-shot risk:**
[The one competitor move that would actually hurt]
**Market dynamics:**
[Tailwinds, headwinds, timing factors]
---
## 6. Current Challenges
**Priority stack-rank:**
1. [Highest priority: product/growth/people/money/operations]
2.
3.
4.
5.
**The avoided decision:**
[What they've been putting off — and why]
**The "one extra day" answer:**
[What they'd actually work on — reveals true priority]
---
## 7. Goals & Ambition
**12-month target:**
[Specific — revenue, product milestone, market position]
**36-month target:**
[Directional — where does this company go]
**Exit orientation:** building to exit | building to run | undecided
**Personal success definition:**
[Separate from company — what does winning look like for them personally]
---
## Key Themes & Implicit Signals
**Patterns observed:**
[What came up repeatedly, what they rushed past, emotional charge on topics]
**Implicit tensions:**
[Gaps between stated and revealed — e.g., "says people are fine, but conflict story suggests otherwise"]
**Watch list:**
[Things to check on in the next update — risks, avoided decisions, relationships to monitor]
---
## Context Metadata
- **Interview conducted:** [DATE]
- **Duration:** [N minutes]
- **Interview type:** full | update
- **Next refresh due:** [DATE + 90 days]
- **Confidence level:** high | medium | low (low = founder was guarded)
FILE:cto-advisor/SKILL.md
---
name: "cto-advisor"
description: "Technical leadership guidance for engineering teams, architecture decisions, and technology strategy. Use when assessing technical debt, scaling engineering teams, evaluating technologies, making architecture decisions, establishing engineering metrics, or when user mentions CTO, tech debt, technical debt, team scaling, architecture decisions, technology evaluation, engineering metrics, DORA metrics, or technology strategy."
license: MIT
metadata:
version: 2.0.0
author: Alireza Rezvani
category: c-level
domain: cto-leadership
updated: 2026-03-05
python-tools: tech_debt_analyzer.py, team_scaling_calculator.py
frameworks: architecture-decisions, engineering-metrics, technology-evaluation
---
# CTO Advisor
Technical leadership frameworks for architecture, engineering teams, technology strategy, and technical decision-making.
## Keywords
CTO, chief technology officer, tech debt, technical debt, architecture, engineering metrics, DORA, team scaling, technology evaluation, build vs buy, cloud migration, platform engineering, AI/ML strategy, system design, incident response, engineering culture
## Quick Start
```bash
python scripts/tech_debt_analyzer.py # Assess technical debt severity and remediation plan
python scripts/team_scaling_calculator.py # Model engineering team growth and cost
```
## Core Responsibilities
### 1. Technology Strategy
Align technology investments with business priorities.
**Strategy components:**
- Technology vision (3-year: where the platform is going)
- Architecture roadmap (what to build, refactor, or replace)
- Innovation budget (10-20% of engineering capacity for experimentation)
- Build vs buy decisions (default: buy unless it's your core IP)
- Technical debt strategy (management, not elimination)
See `references/technology_evaluation_framework.md` for the full evaluation framework.
### 2. Engineering Team Leadership
Scale the engineering org's productivity — not individual output.
**Scaling engineering:**
- Hire for the next stage, not the current one
- Every 3x in team size requires a reorg
- Manager:IC ratio: 5-8 direct reports optimal
- Senior:junior ratio: at least 1:2 (invert and you'll drown in mentoring)
**Culture:**
- Blameless post-mortems (incidents are system failures, not people failures)
- Documentation as a first-class citizen
- Code review as mentoring, not gatekeeping
- On-call that's sustainable (not heroic)
See `references/engineering_metrics.md` for DORA metrics and the engineering health dashboard.
### 3. Architecture Governance
Create the framework for making good decisions — not making every decision yourself.
**Architecture Decision Records (ADRs):**
- Every significant decision gets documented: context, options, decision, consequences
- Decisions are discoverable (not buried in Slack)
- Decisions can be superseded (not permanent)
See `references/architecture_decision_records.md` for ADR templates and the decision review process.
### 4. Vendor & Platform Management
Every vendor is a dependency. Every dependency is a risk.
**Evaluation criteria:** Does it solve a real problem? Can we migrate away? Is the vendor stable? What's the total cost (license + integration + maintenance)?
### 5. Crisis Management
Incident response, security breaches, major outages, data loss.
**Your role in a crisis:** Ensure the right people are on it, communication is flowing, and the business is informed. Post-crisis: blameless retrospective within 48 hours.
## Workflows
### Tech Debt Assessment Workflow
**Step 1 — Run the analyzer**
```bash
python scripts/tech_debt_analyzer.py --output report.json
```
**Step 2 — Interpret results**
The analyzer produces a severity-scored inventory. Review each item against:
- Severity (P0–P3): how much is it blocking velocity or creating risk?
- Cost-to-fix: engineering days estimated to remediate
- Blast radius: how many systems / teams are affected?
**Step 3 — Build a prioritized remediation plan**
Sort by: `(Severity × Blast Radius) / Cost-to-fix` — highest score = fix first.
Group items into: (a) immediate sprint, (b) next quarter, (c) tracked backlog.
**Step 4 — Validate before presenting to stakeholders**
- [ ] Every P0/P1 item has an owner and a target date
- [ ] Cost-to-fix estimates reviewed with the relevant tech lead
- [ ] Debt ratio calculated: maintenance work / total engineering capacity (target: < 25%)
- [ ] Remediation plan fits within capacity (don't promise 40 points of debt reduction in a 2-week sprint)
**Example output — Tech Debt Inventory:**
```
Item | Severity | Cost-to-Fix | Blast Radius | Priority Score
----------------------|----------|-------------|--------------|---------------
Auth service (v1 API) | P1 | 8 days | 6 services | HIGH
Unindexed DB queries | P2 | 3 days | 2 services | MEDIUM
Legacy deploy scripts | P3 | 5 days | 1 service | LOW
```
---
### ADR Creation Workflow
**Step 1 — Identify the decision**
Trigger an ADR when: the decision affects more than one team, is hard to reverse, or has cost/risk implications > 1 sprint of effort.
**Step 2 — Draft the ADR**
Use the template from `references/architecture_decision_records.md`:
```
Title: [Short noun phrase]
Status: Proposed | Accepted | Superseded
Context: What is the problem? What constraints exist?
Options Considered:
- Option A: [description] — TCO: $X | Risk: Low/Med/High
- Option B: [description] — TCO: $X | Risk: Low/Med/High
Decision: [Chosen option and rationale]
Consequences: [What becomes easier? What becomes harder?]
```
**Step 3 — Validation checkpoint (before finalizing)**
- [ ] All options include a 3-year TCO estimate
- [ ] At least one "do nothing" or "buy" alternative is documented
- [ ] Affected team leads have reviewed and signed off
- [ ] Consequences section addresses reversibility and migration path
- [ ] ADR is committed to the repository (not left in a doc or Slack thread)
**Step 4 — Communicate and close**
Share the accepted ADR in the engineering all-hands or architecture sync. Link it from the relevant service's README.
---
### Build vs Buy Analysis Workflow
**Step 1 — Define requirements** (functional + non-functional)
**Step 2 — Identify candidate vendors or internal build scope**
**Step 3 — Score each option:**
```
Criterion | Weight | Build Score | Vendor A Score | Vendor B Score
-----------------------|--------|-------------|----------------|---------------
Solves core problem | 30% | 9 | 8 | 7
Migration risk | 20% | 2 (low risk)| 7 | 6
3-year TCO | 25% | $X | $Y | $Z
Vendor stability | 15% | N/A | 8 | 5
Integration effort | 10% | 3 | 7 | 8
```
**Step 4 — Default rule:** Buy unless it is core IP or no vendor meets ≥ 70% of requirements.
**Step 5 — Document the decision as an ADR** (see ADR workflow above).
## Key Questions a CTO Asks
- "What's our biggest technical risk right now — not the most annoying, the most dangerous?"
- "If we 10x our traffic tomorrow, what breaks first?"
- "How much of our engineering time goes to maintenance vs new features?"
- "What would a new engineer say about our codebase after their first week?"
- "Which technical decision from 2 years ago is hurting us most today?"
- "Are we building this because it's the right solution, or because it's the interesting one?"
- "What's our bus factor on critical systems?"
## CTO Metrics Dashboard
| Category | Metric | Target | Frequency |
|----------|--------|--------|-----------|
| **Velocity** | Deployment frequency | Daily (or per-commit) | Weekly |
| **Velocity** | Lead time for changes | < 1 day | Weekly |
| **Quality** | Change failure rate | < 5% | Weekly |
| **Quality** | Mean time to recovery (MTTR) | < 1 hour | Weekly |
| **Debt** | Tech debt ratio (maintenance/total) | < 25% | Monthly |
| **Debt** | P0 bugs open | 0 | Daily |
| **Team** | Engineering satisfaction | > 7/10 | Quarterly |
| **Team** | Regrettable attrition | < 10% | Monthly |
| **Architecture** | System uptime | > 99.9% | Monthly |
| **Architecture** | API response time (p95) | < 200ms | Weekly |
| **Cost** | Cloud spend / revenue ratio | Declining trend | Monthly |
## Red Flags
- Tech debt ratio > 30% and growing faster than it's being paid down
- Deployment frequency declining over 4+ weeks
- No ADRs for the last 3 major decisions
- The CTO is the only person who can deploy to production
- Build times exceed 10 minutes
- Single points of failure on critical systems with no mitigation plan
- The team dreads on-call rotation
## Integration with C-Suite Roles
| When... | CTO works with... | To... |
|---------|-------------------|-------|
| Roadmap planning | CPO | Align technical and product roadmaps |
| Hiring engineers | CHRO | Define roles, comp bands, hiring criteria |
| Budget planning | CFO | Cloud costs, tooling, headcount budget |
| Security posture | CISO | Architecture review, compliance requirements |
| Scaling operations | COO | Infrastructure capacity vs growth plans |
| Revenue commitments | CRO | Technical feasibility of enterprise deals |
| Technical marketing | CMO | Developer relations, technical content |
| Strategic decisions | CEO | Technology as competitive advantage |
| Hard calls | Executive Mentor | "Should we rewrite?" "Should we switch stacks?" |
## Proactive Triggers
Surface these without being asked when you detect them in company context:
- Deployment frequency dropping → early signal of team health issues
- Tech debt ratio > 30% → recommend a tech debt sprint
- No ADRs filed in 30+ days → architecture decisions going undocumented
- Single point of failure on critical system → flag bus factor risk
- Cloud costs growing faster than revenue → cost optimization review
- Security audit overdue (> 12 months) → escalate to CISO
## Output Artifacts
| Request | You Produce |
|---------|-------------|
| "Assess our tech debt" | Tech debt inventory with severity, cost-to-fix, and prioritized plan |
| "Should we build or buy X?" | Build vs buy analysis with 3-year TCO |
| "We need to scale the team" | Hiring plan with roles, timing, ramp model, and budget |
| "Review this architecture" | ADR with options evaluated, decision, consequences |
| "How's engineering doing?" | Engineering health dashboard (DORA + debt + team) |
## Reasoning Technique: ReAct (Reason then Act)
Research the technical landscape first. Analyze options against constraints (time, team skill, cost, risk). Then recommend action. Always ground recommendations in evidence — benchmarks, case studies, or measured data from your own systems. "I think" is not enough — show the data.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
## Resources
- `references/technology_evaluation_framework.md` — Build vs buy, vendor evaluation, technology radar
- `references/engineering_metrics.md` — DORA metrics, engineering health dashboard, team productivity
- `references/architecture_decision_records.md` — ADR templates, decision governance, review process
FILE:cto-advisor/references/architecture_decision_records.md
# Architecture Decision Records (ADR) Framework
## What is an ADR?
Architecture Decision Records capture important architectural decisions made along with their context and consequences. They help maintain institutional knowledge and explain why systems are built the way they are.
## ADR Template
### ADR-[NUMBER]: [TITLE]
**Date**: YYYY-MM-DD
**Status**: [Proposed | Accepted | Deprecated | Superseded]
**Deciders**: [List of people involved in decision]
**Technical Story**: [Ticket/Issue reference]
#### Context and Problem Statement
[Describe the context and problem that needs to be solved. What are we trying to achieve?]
#### Decision Drivers
- [Driver 1: e.g., Performance requirements]
- [Driver 2: e.g., Time to market]
- [Driver 3: e.g., Team expertise]
- [Driver 4: e.g., Cost constraints]
#### Considered Options
1. **Option 1: [Name]**
2. **Option 2: [Name]**
3. **Option 3: [Name]**
#### Decision Outcome
**Chosen option**: "[Option Name]", because [justification]
##### Positive Consequences
- [Consequence 1]
- [Consequence 2]
##### Negative Consequences
- [Risk 1 and mitigation]
- [Risk 2 and mitigation]
#### Pros and Cons of Options
##### Option 1: [Name]
- **Pros**:
- [Advantage 1]
- [Advantage 2]
- **Cons**:
- [Disadvantage 1]
- [Disadvantage 2]
##### Option 2: [Name]
[Repeat structure]
#### Links
- [Related ADRs]
- [Documentation]
- [Research/PoCs]
---
## Example ADRs
### ADR-001: Microservices Architecture
**Date**: 2024-01-15
**Status**: Accepted
**Deciders**: CTO, VP Engineering, Tech Leads
**Technical Story**: ARCH-001
#### Context and Problem Statement
Our monolithic application is becoming difficult to scale and deploy. Different teams are stepping on each other's toes, and deployment cycles are getting longer. We need to decide on our architectural approach for the next 3-5 years.
#### Decision Drivers
- Need for independent team deployment
- Requirement to scale different components independently
- Different components have different performance characteristics
- Team size growing from 25 to 75+ engineers
- Need to support multiple technology stacks
#### Considered Options
1. **Keep Monolith**: Continue with current architecture
2. **Modular Monolith**: Break into modules but single deployment
3. **Microservices**: Full service-oriented architecture
4. **Serverless**: Function-as-a-Service approach
#### Decision Outcome
**Chosen option**: "Microservices", because it best supports our team autonomy needs and scaling requirements, despite added complexity.
##### Positive Consequences
- Teams can deploy independently
- Services can scale based on individual needs
- Technology diversity is possible
- Fault isolation improved
##### Negative Consequences
- Increased operational complexity - Mitigated by investing in DevOps
- Network latency between services - Mitigated by careful service boundaries
- Data consistency challenges - Mitigated by event sourcing patterns
---
### ADR-002: Container Orchestration Platform
**Date**: 2024-02-01
**Status**: Accepted
**Deciders**: CTO, DevOps Lead, Platform Team
**Technical Story**: INFRA-045
#### Context and Problem Statement
With the move to microservices (ADR-001), we need a container orchestration platform to manage deployment, scaling, and operations of application containers.
#### Decision Drivers
- Need for automated deployment and scaling
- High availability requirements (99.9% SLA)
- Multi-cloud strategy (avoid vendor lock-in)
- Team familiarity and ecosystem maturity
- Cost considerations
#### Considered Options
1. **Kubernetes**: Industry standard, self-managed
2. **Amazon ECS**: AWS-native solution
3. **Docker Swarm**: Simpler alternative
4. **Nomad**: HashiCorp solution
#### Decision Outcome
**Chosen option**: "Kubernetes", because of its maturity, ecosystem, and multi-cloud support.
##### Positive Consequences
- Industry standard with huge ecosystem
- Multi-cloud compatible
- Strong community support
- Extensive tooling available
##### Negative Consequences
- Steep learning curve - Mitigated by training and hiring
- Operational complexity - Mitigated by managed Kubernetes (EKS/GKE)
---
### ADR-003: API Gateway Strategy
**Date**: 2024-03-15
**Status**: Accepted
**Deciders**: CTO, Security Lead, API Team
**Technical Story**: API-101
#### Context and Problem Statement
With multiple microservices, we need a unified entry point for external clients that handles cross-cutting concerns like authentication, rate limiting, and monitoring.
#### Decision Drivers
- Security requirements (OAuth2, API keys)
- Need for rate limiting and throttling
- Monitoring and analytics requirements
- Developer experience for API consumers
- Performance (sub-100ms overhead)
#### Considered Options
1. **Kong**: Open-source, plugin ecosystem
2. **AWS API Gateway**: Managed service
3. **Istio/Envoy**: Service mesh approach
4. **Build Custom**: In-house solution
#### Decision Outcome
**Chosen option**: "Kong", because of its flexibility and plugin ecosystem while avoiding vendor lock-in.
---
## Common Architecture Decisions
### 1. Frontend Architecture
- **Single Page Application (SPA)** vs **Server-Side Rendering (SSR)** vs **Static Site Generation (SSG)**
- **React** vs **Vue** vs **Angular** vs **Svelte**
- **Monorepo** vs **Polyrepo**
- **Micro-frontends** vs **Monolithic frontend**
### 2. Backend Architecture
- **Monolith** vs **Microservices** vs **Serverless**
- **REST** vs **GraphQL** vs **gRPC**
- **Synchronous** vs **Asynchronous** communication
- **Event-driven** vs **Request-response**
### 3. Data Architecture
- **SQL** vs **NoSQL** vs **NewSQL**
- **Single database** vs **Database per service**
- **CQRS** vs **Traditional CRUD**
- **Event Sourcing** vs **State-based storage**
### 4. Infrastructure Decisions
- **Cloud provider**: AWS vs Azure vs GCP vs Multi-cloud
- **Containers** vs **VMs** vs **Serverless**
- **Kubernetes** vs **ECS** vs **Cloud Run**
- **Self-hosted** vs **Managed services**
### 5. Development Practices
- **Continuous Deployment** vs **Continuous Delivery**
- **Feature flags** vs **Branch-based deployment**
- **Blue-green** vs **Canary** vs **Rolling deployment**
- **GitFlow** vs **GitHub Flow** vs **GitLab Flow**
## ADR Best Practices
### Writing Good ADRs
1. **Keep them short**: 1-2 pages maximum
2. **Be specific**: Include concrete examples
3. **Document why, not what**: Focus on reasoning
4. **Include all options**: Even obviously bad ones
5. **Be honest about drawbacks**: Every decision has trade-offs
### When to Write ADRs
Write an ADR when:
- The decision has significant impact
- Multiple options were seriously considered
- The decision is hard to reverse
- You find yourself explaining the same decision repeatedly
- There's disagreement about the approach
### ADR Lifecycle
1. **Proposed**: Under discussion
2. **Accepted**: Decision made and being implemented
3. **Deprecated**: No longer relevant but kept for history
4. **Superseded**: Replaced by another ADR
### Storage and Discovery
- Store ADRs in your main repository under `docs/architecture/decisions/`
- Use consistent numbering (ADR-001, ADR-002, etc.)
- Create an index file linking all ADRs
- Reference ADRs in code comments where relevant
- Review ADRs regularly (quarterly) for relevance
## Decision Evaluation Framework
### Technical Factors (40%)
- Performance impact
- Scalability potential
- Security implications
- Maintainability
- Technical debt
### Business Factors (30%)
- Time to market
- Cost (initial and ongoing)
- Revenue impact
- Competitive advantage
- Regulatory compliance
### Team Factors (30%)
- Current expertise
- Learning curve
- Hiring availability
- Team preference
- Training requirements
## Anti-patterns to Avoid
1. **Decision by Committee**: Too many stakeholders leading to compromise solutions
2. **Analysis Paralysis**: Over-analyzing instead of deciding
3. **Resume-Driven Development**: Choosing tech for personal goals
4. **Hype-Driven Development**: Choosing the newest/coolest tech
5. **Not-Invented-Here**: Rejecting external solutions by default
6. **Vendor Lock-in**: Over-dependence on proprietary solutions
7. **Premature Optimization**: Solving problems you don't have yet
8. **Under-documentation**: Not capturing the "why" behind decisions
## Review Checklist
Before finalizing an ADR, ensure:
- [ ] Problem is clearly stated
- [ ] All realistic options are considered
- [ ] Trade-offs are honestly evaluated
- [ ] Decision rationale is clear
- [ ] Consequences are identified
- [ ] Mitigation strategies are defined
- [ ] Success metrics are established
- [ ] Review date is set (if applicable)
FILE:cto-advisor/references/engineering_metrics.md
# Engineering Metrics & KPIs Guide
## Metrics Framework
### DORA Metrics (DevOps Research and Assessment)
#### 1. Deployment Frequency
- **Definition**: How often code is deployed to production
- **Target**:
- Elite: Multiple deploys per day
- High: Weekly to monthly
- Medium: Monthly to bi-annually
- Low: Less than bi-annually
- **Measurement**: Deployments per day/week/month
- **Improvement**: Smaller batch sizes, feature flags, CI/CD
#### 2. Lead Time for Changes
- **Definition**: Time from code commit to production
- **Target**:
- Elite: Less than 1 hour
- High: 1 day to 1 week
- Medium: 1 week to 1 month
- Low: More than 1 month
- **Measurement**: Median time from commit to deploy
- **Improvement**: Automation, parallel testing, smaller changes
#### 3. Mean Time to Recovery (MTTR)
- **Definition**: Time to restore service after incident
- **Target**:
- Elite: Less than 1 hour
- High: Less than 1 day
- Medium: 1 day to 1 week
- Low: More than 1 week
- **Measurement**: Average incident resolution time
- **Improvement**: Monitoring, rollback capability, runbooks
#### 4. Change Failure Rate
- **Definition**: Percentage of changes causing failures
- **Target**:
- Elite: 0-15%
- High: 16-30%
- Medium/Low: >30%
- **Measurement**: Failed deploys / Total deploys
- **Improvement**: Testing, code review, gradual rollouts
### Engineering Productivity Metrics
#### Code Quality
| Metric | Formula | Target | Action if Below |
|--------|---------|--------|-----------------|
| Test Coverage | Tests / Total Code | >80% | Add unit tests |
| Code Review Coverage | Reviewed PRs / Total PRs | 100% | Enforce review policy |
| Technical Debt Ratio | Debt / Development Time | <10% | Dedicate debt sprints |
| Cyclomatic Complexity | Per function/method | <10 | Refactor complex code |
| Code Duplication | Duplicate Lines / Total | <5% | Extract common code |
#### Development Velocity
| Metric | Formula | Target | Action if Below |
|--------|---------|--------|-----------------|
| Sprint Velocity | Story Points / Sprint | Stable ±10% | Review estimation |
| Cycle Time | Start to Done Time | <5 days | Reduce WIP |
| PR Merge Time | Open to Merge | <24 hours | Smaller PRs |
| Build Time | Code to Artifact | <10 minutes | Optimize pipeline |
| Test Execution Time | Full Test Suite | <30 minutes | Parallelize tests |
#### Team Health
| Metric | Formula | Target | Action if Below |
|--------|---------|--------|-----------------|
| On-call Incidents | Incidents / Week | <5 | Improve monitoring |
| Bug Escape Rate | Prod Bugs / Release | <5% | Improve testing |
| Unplanned Work | Unplanned / Total | <20% | Better planning |
| Meeting Time | Meetings / Total Time | <20% | Reduce meetings |
| Focus Time | Uninterrupted Hours | >4h/day | Block calendars |
### Business Impact Metrics
#### System Performance
| Metric | Description | Target | Business Impact |
|--------|-------------|--------|-----------------|
| Uptime | System availability | 99.9%+ | Revenue protection |
| Page Load Time | Time to interactive | <3s | User retention |
| API Response Time | P95 latency | <200ms | User experience |
| Error Rate | Errors / Requests | <0.1% | Customer satisfaction |
| Throughput | Requests / Second | Per requirement | Scalability |
#### Product Delivery
| Metric | Description | Target | Business Impact |
|--------|-------------|--------|-----------------|
| Feature Delivery Rate | Features / Quarter | Per roadmap | Market competitiveness |
| Time to Market | Idea to Production | <3 months | First mover advantage |
| Customer Defect Rate | Customer Bugs / Month | <10 | Customer satisfaction |
| Feature Adoption | Users / Feature | >50% | ROI validation |
| NPS from Engineering | Customer Score | >50 | Product quality |
## Metrics Dashboards
### Executive Dashboard (Weekly)
```
┌─────────────────────────────────────┐
│ EXECUTIVE METRICS │
├─────────────────────────────────────┤
│ Uptime: 99.97% ✓ │
│ Sprint Velocity: 142 pts ✓ │
│ Deployment Frequency: 3.2/day ✓ │
│ Lead Time: 4.2 hrs ✓ │
│ MTTR: 47 min ✓ │
│ Change Failure Rate: 8.3% ✓ │
│ │
│ Team Health: 8.2/10 │
│ Tech Debt Ratio: 12% ⚠ │
│ Feature Delivery: 85% ✓ │
└─────────────────────────────────────┘
```
### Team Dashboard (Daily)
```
┌─────────────────────────────────────┐
│ TEAM METRICS │
├─────────────────────────────────────┤
│ Current Sprint: │
│ Completed: 65/100 pts (65%) │
│ In Progress: 20 pts │
│ Days Left: 3 │
│ │
│ PR Queue: 8 pending │
│ Build Status: ✓ Passing │
│ Test Coverage: 82.3% │
│ Open Incidents: 2 (P2, P3) │
│ │
│ On-call Load: 3 pages this week │
└─────────────────────────────────────┘
```
### Individual Dashboard (Daily)
```
┌─────────────────────────────────────┐
│ DEVELOPER METRICS │
├─────────────────────────────────────┤
│ This Week: │
│ PRs Merged: 8 │
│ Code Reviews: 12 │
│ Commits: 23 │
│ Focus Time: 22.5 hrs │
│ │
│ Quality: │
│ Test Coverage: 87% │
│ Code Review Feedback: 95% ✓ │
│ Bug Introduction Rate: 0% │
└─────────────────────────────────────┘
```
## Implementation Guide
### Phase 1: Foundation (Month 1)
1. **Basic Metrics**
- Deployment frequency
- Build success rate
- Uptime/availability
- Team velocity
2. **Tools Setup**
- CI/CD instrumentation
- Basic monitoring
- Time tracking
### Phase 2: Quality (Month 2)
1. **Quality Metrics**
- Test coverage
- Code review metrics
- Bug rates
- Technical debt
2. **Tool Integration**
- Static analysis
- Test reporting
- Code quality gates
### Phase 3: Performance (Month 3)
1. **Performance Metrics**
- DORA metrics complete
- System performance
- API metrics
- Database metrics
2. **Advanced Monitoring**
- APM tools
- Distributed tracing
- Custom dashboards
### Phase 4: Optimization (Ongoing)
1. **Advanced Analytics**
- Predictive metrics
- Trend analysis
- Anomaly detection
- Correlation analysis
## Metric Anti-patterns
### What NOT to Measure
❌ **Lines of Code**: Encourages bloat
❌ **Hours Worked**: Promotes presenteeism
❌ **Individual Velocity**: Creates competition
❌ **Bug Count Without Context**: Discourages risk-taking
❌ **Commit Count**: Encourages tiny commits
### Goodhart's Law
"When a measure becomes a target, it ceases to be a good measure"
**Examples**:
- Optimizing test coverage → Writing meaningless tests
- Reducing bug count → Not reporting bugs
- Increasing velocity → Inflating estimates
- Reducing meeting time → Skipping important discussions
### How to Avoid Gaming
1. **Use Multiple Metrics**: No single metric tells the whole story
2. **Focus on Trends**: Not absolute numbers
3. **Combine Leading and Lagging**: Balance predictive and historical
4. **Regular Review**: Adjust metrics that are being gamed
5. **Team Ownership**: Let teams choose their metrics
## OKR Framework for Engineering
### Company Level OKRs
**Objective**: Deliver exceptional product quality
**Key Results**:
- KR1: Achieve 99.95% uptime (from 99.9%)
- KR2: Reduce customer-reported bugs by 50%
- KR3: Improve deployment frequency to 10x/day
### Engineering OKRs
**Objective**: Build scalable, reliable infrastructure
**Key Results**:
- KR1: Migrate 80% of services to Kubernetes
- KR2: Reduce MTTR to <30 minutes
- KR3: Achieve 85% test coverage
### Team OKRs
**Objective**: Improve developer productivity
**Key Results**:
- KR1: Reduce build time to <5 minutes
- KR2: Automate 90% of deployment process
- KR3: Reduce PR review time to <4 hours
## Reporting Templates
### Monthly Engineering Report
```markdown
# Engineering Report - [Month Year]
## Executive Summary
- Key Achievement: [Highlight]
- Main Challenge: [Issue and resolution]
- Next Month Focus: [Priority]
## DORA Metrics
| Metric | This Month | Last Month | Target | Status |
|--------|------------|------------|--------|--------|
| Deploy Frequency | X/day | Y/day | Z/day | ✓/⚠/✗ |
| Lead Time | X hrs | Y hrs | <Z hrs | ✓/⚠/✗ |
| MTTR | X min | Y min | <Z min | ✓/⚠/✗ |
| Change Failure | X% | Y% | <Z% | ✓/⚠/✗ |
## Team Performance
- Velocity: X story points (Y% of plan)
- Sprint Completion: X%
- Unplanned Work: X%
## Quality Metrics
- Test Coverage: X% (Δ Y%)
- Customer Bugs: X (Δ Y)
- Code Review Coverage: X%
## Highlights
1. [Major feature or improvement]
2. [Technical achievement]
3. [Process improvement]
## Challenges & Solutions
1. Challenge: [Issue]
Solution: [Action taken]
## Next Month Priorities
1. [Priority 1]
2. [Priority 2]
3. [Priority 3]
```
### Quarterly Business Review
```markdown
# Engineering QBR - Q[X] [Year]
## Strategic Alignment
- Business Goal: [Goal]
- Engineering Contribution: [How engineering supported]
- Impact: [Measurable outcome]
## Quarterly Metrics
### Delivery
- Features Shipped: X of Y planned (Z%)
- Major Releases: [List]
- Technical Debt Reduced: X%
### Reliability
- Uptime: X%
- Incidents: X (PY critical, PZ major)
- Customer Impact: [Description]
### Efficiency
- Cost per Transaction: $X (Δ Y%)
- Infrastructure Cost: $X (Δ Y%)
- Engineering Cost per Feature: $X
## Team Growth
- Headcount: Start: X → End: Y
- Attrition: X%
- Key Hires: [Roles]
## Innovation
- Patents Filed: X
- Open Source Contributions: X
- Hackathon Projects: X
## Lessons Learned
1. [What worked well]
2. [What didn't work]
3. [What we're changing]
## Next Quarter Focus
1. [Strategic Initiative 1]
2. [Strategic Initiative 2]
3. [Strategic Initiative 3]
```
## Tool Recommendations
### Metrics Collection
- **DataDog**: Comprehensive monitoring
- **New Relic**: Application performance
- **Grafana + Prometheus**: Open source stack
- **CloudWatch**: AWS native
### Engineering Analytics
- **LinearB**: Developer productivity
- **Velocity**: Engineering metrics
- **Sleuth**: DORA metrics
- **Swarmia**: Engineering insights
### Project Tracking
- **Jira**: Issue tracking
- **Linear**: Modern issue tracking
- **Azure DevOps**: Microsoft ecosystem
- **GitHub Projects**: Integrated with code
### Incident Management
- **PagerDuty**: On-call management
- **Opsgenie**: Incident response
- **StatusPage**: Status communication
- **FireHydrant**: Incident command
## Success Indicators
### Healthy Engineering Organization
✓ DORA metrics improving quarter-over-quarter
✓ Team satisfaction >8/10
✓ Attrition <10% annually
✓ On-time delivery >80%
✓ Technical debt <15% of capacity
✓ Innovation time >20%
### Warning Signs
⚠️ Increasing MTTR trend
⚠️ Declining velocity
⚠️ Rising bug escape rate
⚠️ Increasing unplanned work
⚠️ Growing PR queue
⚠️ Decreasing test coverage
### Crisis Indicators
🚨 Multiple production incidents per week
🚨 Team satisfaction <6/10
🚨 Attrition >20%
🚨 Technical debt >30%
🚨 No deployments for >1 week
🚨 Customer escalations increasing
FILE:cto-advisor/references/technology_evaluation_framework.md
# Technology Evaluation Framework
## Evaluation Process
### Phase 1: Requirements Gathering (Week 1)
#### Functional Requirements
- Core features needed
- Integration requirements
- Performance requirements
- Scalability needs
- Security requirements
#### Non-Functional Requirements
- Usability/Developer experience
- Documentation quality
- Community support
- Vendor stability
- Compliance needs
#### Constraints
- Budget limitations
- Timeline constraints
- Team expertise
- Existing technology stack
- Regulatory requirements
### Phase 2: Market Research (Week 1-2)
#### Identify Candidates
1. Industry leaders (Gartner Magic Quadrant)
2. Open-source alternatives
3. Emerging solutions
4. Build vs Buy analysis
#### Initial Filtering
- Eliminate options not meeting hard requirements
- Remove options outside budget
- Focus on 3-5 top candidates
### Phase 3: Deep Evaluation (Week 2-4)
#### Technical Evaluation
- Proof of Concept (PoC)
- Performance benchmarks
- Security assessment
- Integration testing
- Scalability testing
#### Business Evaluation
- Total Cost of Ownership (TCO)
- Return on Investment (ROI)
- Vendor assessment
- Risk analysis
- Exit strategy
### Phase 4: Decision (Week 4)
## Evaluation Criteria Matrix
### Technical Criteria (40%)
| Criterion | Weight | Description | Scoring Guide |
|-----------|--------|-------------|---------------|
| **Performance** | 10% | Speed, throughput, latency | 5: Exceeds requirements<br>3: Meets requirements<br>1: Below requirements |
| **Scalability** | 10% | Ability to grow with needs | 5: Linear scalability<br>3: Some limitations<br>1: Hard limits |
| **Reliability** | 8% | Uptime, fault tolerance | 5: 99.99% SLA<br>3: 99.9% SLA<br>1: <99% SLA |
| **Security** | 8% | Security features, compliance | 5: Exceeds standards<br>3: Meets standards<br>1: Concerns exist |
| **Integration** | 4% | API quality, compatibility | 5: Native integration<br>3: Good APIs<br>1: Limited integration |
### Business Criteria (30%)
| Criterion | Weight | Description | Scoring Guide |
|-----------|--------|-------------|---------------|
| **Cost** | 10% | TCO including licenses, operation | 5: Under budget by >20%<br>3: Within budget<br>1: Over budget |
| **ROI** | 8% | Value generation potential | 5: <6 month payback<br>3: <12 month payback<br>1: >24 month payback |
| **Vendor Stability** | 6% | Financial health, market position | 5: Market leader<br>3: Established player<br>1: Startup/uncertain |
| **Support Quality** | 6% | Support availability, SLAs | 5: 24/7 premium support<br>3: Business hours<br>1: Community only |
### Operational Criteria (30%)
| Criterion | Weight | Description | Scoring Guide |
|-----------|--------|-------------|---------------|
| **Ease of Use** | 8% | Learning curve, UX | 5: Intuitive<br>3: Moderate learning<br>1: Steep curve |
| **Documentation** | 7% | Quality, completeness | 5: Excellent docs<br>3: Adequate docs<br>1: Poor docs |
| **Community** | 7% | Size, activity, resources | 5: Large, active<br>3: Moderate<br>1: Small/inactive |
| **Maintenance** | 8% | Operational overhead | 5: Fully managed<br>3: Some maintenance<br>1: High maintenance |
## Vendor Evaluation Template
### Vendor Profile
- **Company Name**:
- **Founded**:
- **Headquarters**:
- **Employees**:
- **Revenue**:
- **Funding** (if applicable):
- **Key Customers**:
### Product Assessment
#### Strengths
- [ ] Market leader position
- [ ] Strong feature set
- [ ] Good performance
- [ ] Excellent support
- [ ] Active development
#### Weaknesses
- [ ] Price point
- [ ] Learning curve
- [ ] Limited customization
- [ ] Vendor lock-in
- [ ] Missing features
#### Opportunities
- [ ] Roadmap alignment
- [ ] Partnership potential
- [ ] Training availability
- [ ] Professional services
#### Threats
- [ ] Competitive alternatives
- [ ] Market changes
- [ ] Technology shifts
- [ ] Acquisition risk
### Financial Analysis
#### Cost Breakdown
| Component | Year 1 | Year 2 | Year 3 | Total |
|-----------|--------|--------|--------|-------|
| Licensing | $ | $ | $ | $ |
| Implementation | $ | $ | $ | $ |
| Training | $ | $ | $ | $ |
| Support | $ | $ | $ | $ |
| Infrastructure | $ | $ | $ | $ |
| **Total** | **$** | **$** | **$** | **$** |
#### ROI Calculation
- **Cost Savings**:
- Reduced manual work: $/year
- Efficiency gains: $/year
- Error reduction: $/year
- **Revenue Impact**:
- New capabilities: $/year
- Faster time to market: $/year
- **Payback Period**: X months
### Risk Assessment
| Risk | Probability | Impact | Mitigation |
|------|------------|--------|------------|
| Vendor goes out of business | Low/Med/High | Low/Med/High | Strategy |
| Technology becomes obsolete | | | |
| Integration difficulties | | | |
| Team adoption challenges | | | |
| Budget overrun | | | |
| Performance issues | | | |
## Build vs Buy Decision Framework
### When to Build
**Advantages**:
- Full control over features
- No vendor lock-in
- Potential competitive advantage
- Perfect fit for requirements
- No licensing costs
**Build when**:
- Core business differentiator
- Unique requirements
- Long-term investment
- Have expertise in-house
- No suitable solutions exist
**Hidden Costs**:
- Development time
- Maintenance burden
- Security responsibility
- Documentation needs
- Training requirements
### When to Buy
**Advantages**:
- Faster time to market
- Proven solution
- Vendor support
- Regular updates
- Shared development costs
**Buy when**:
- Commodity functionality
- Standard requirements
- Limited internal resources
- Need quick solution
- Good options available
**Hidden Costs**:
- Customization limits
- Vendor lock-in
- Integration effort
- Training needs
- Scaling costs
### When to Adopt Open Source
**Advantages**:
- No licensing costs
- Community support
- Transparency
- Customizable
- No vendor lock-in
**Adopt when**:
- Strong community exists
- Standard solution needed
- Have technical expertise
- Can contribute back
- Long-term stability needed
**Hidden Costs**:
- Support costs
- Security responsibility
- Upgrade management
- Integration effort
- Potential consulting needs
## Proof of Concept Guidelines
### PoC Scope
1. **Duration**: 2-4 weeks
2. **Team**: 2-3 engineers
3. **Environment**: Isolated/sandbox
4. **Data**: Representative sample
### Success Criteria
- [ ] Core use cases demonstrated
- [ ] Performance benchmarks met
- [ ] Integration points tested
- [ ] Security requirements validated
- [ ] Team feedback positive
### PoC Checklist
- [ ] Environment setup documented
- [ ] Test scenarios defined
- [ ] Metrics collection automated
- [ ] Team training completed
- [ ] Results documented
### PoC Report Template
```markdown
# PoC Report: [Technology Name]
## Executive Summary
- **Recommendation**: [Proceed/Stop/Investigate Further]
- **Confidence Level**: [High/Medium/Low]
- **Key Finding**: [One sentence summary]
## Test Results
### Functional Tests
| Test Case | Result | Notes |
|-----------|--------|-------|
| | Pass/Fail | |
### Performance Tests
| Metric | Target | Actual | Status |
|--------|--------|--------|---------|
| Response Time | <100ms | Xms | ✓/✗ |
| Throughput | >1000 req/s | X req/s | ✓/✗ |
| CPU Usage | <70% | X% | ✓/✗ |
| Memory Usage | <4GB | XGB | ✓/✗ |
### Integration Tests
| System | Status | Effort |
|--------|--------|--------|
| Database | ✓/✗ | Low/Med/High |
| API Gateway | ✓/✗ | Low/Med/High |
| Authentication | ✓/✗ | Low/Med/High |
## Team Feedback
- **Ease of Use**: [1-5 rating]
- **Documentation**: [1-5 rating]
- **Would Recommend**: [Yes/No]
## Risks Identified
1. [Risk and mitigation]
2. [Risk and mitigation]
## Next Steps
1. [Action item]
2. [Action item]
```
## Technology Categories
### Development Platforms
- **Languages**: TypeScript, Python, Go, Rust, Java
- **Frameworks**: React, Node.js, Spring, Django, FastAPI
- **Mobile**: React Native, Flutter, Swift, Kotlin
- **Evaluation Focus**: Developer productivity, ecosystem, performance
### Databases
- **SQL**: PostgreSQL, MySQL, SQL Server
- **NoSQL**: MongoDB, Cassandra, DynamoDB
- **NewSQL**: CockroachDB, Vitess, TiDB
- **Evaluation Focus**: Performance, scalability, consistency, operations
### Infrastructure
- **Cloud**: AWS, GCP, Azure
- **Containers**: Docker, Kubernetes, Nomad
- **Serverless**: Lambda, Cloud Functions, Vercel
- **Evaluation Focus**: Cost, scalability, vendor lock-in, operations
### Monitoring & Observability
- **APM**: DataDog, New Relic, AppDynamics
- **Logging**: ELK Stack, Splunk, CloudWatch
- **Metrics**: Prometheus, Grafana, CloudWatch
- **Evaluation Focus**: Coverage, cost, integration, insights
### Security
- **SAST**: Sonarqube, Checkmarx, Veracode
- **DAST**: OWASP ZAP, Burp Suite
- **Secrets**: Vault, AWS Secrets Manager
- **Evaluation Focus**: Coverage, false positives, integration
### DevOps Tools
- **CI/CD**: Jenkins, GitLab CI, GitHub Actions
- **IaC**: Terraform, CloudFormation, Pulumi
- **Configuration**: Ansible, Chef, Puppet
- **Evaluation Focus**: Flexibility, integration, learning curve
## Continuous Evaluation
### Quarterly Reviews
- Technology landscape changes
- Performance against expectations
- Cost optimization opportunities
- Team satisfaction
- Market alternatives
### Annual Assessment
- Full technology stack review
- Vendor relationship evaluation
- Strategic alignment check
- Technical debt assessment
- Roadmap planning
### Deprecation Planning
- Migration strategy
- Timeline definition
- Risk assessment
- Communication plan
- Success metrics
## Decision Documentation
Always document:
1. **Why** the technology was chosen
2. **Who** was involved in the decision
3. **When** the decision was made
4. **What** alternatives were considered
5. **How** success will be measured
Use Architecture Decision Records (ADRs) for significant technology choices.
FILE:cto-advisor/scripts/team_scaling_calculator.py
#!/usr/bin/env python3
"""
Engineering Team Scaling Calculator - Optimize team growth and structure
"""
import json
import math
from typing import Dict, List, Tuple
class TeamScalingCalculator:
def __init__(self):
self.conway_factor = 1.5 # Conway's Law impact factor
self.brooks_factor = 0.75 # Brooks' Law diminishing returns
# Optimal team structures based on size
self.team_structures = {
'startup': {'min': 1, 'max': 10, 'structure': 'flat'},
'growth': {'min': 11, 'max': 50, 'structure': 'team_leads'},
'scale': {'min': 51, 'max': 150, 'structure': 'departments'},
'enterprise': {'min': 151, 'max': 9999, 'structure': 'divisions'}
}
# Role ratios for balanced teams
self.role_ratios = {
'engineering_manager': 0.125, # 1:8 ratio
'tech_lead': 0.167, # 1:6 ratio
'senior_engineer': 0.3,
'mid_engineer': 0.4,
'junior_engineer': 0.2,
'devops': 0.1,
'qa': 0.15,
'product_manager': 0.1,
'designer': 0.08,
'data_engineer': 0.05
}
def calculate_scaling_plan(self, current_state: Dict, growth_targets: Dict) -> Dict:
"""Calculate optimal scaling plan"""
results = {
'current_analysis': self._analyze_current_state(current_state),
'growth_timeline': self._create_growth_timeline(current_state, growth_targets),
'hiring_plan': {},
'team_structure': {},
'budget_projection': {},
'risk_factors': [],
'recommendations': []
}
# Generate hiring plan
results['hiring_plan'] = self._generate_hiring_plan(
current_state,
growth_targets
)
# Design team structure
results['team_structure'] = self._design_team_structure(
growth_targets['target_headcount']
)
# Calculate budget
results['budget_projection'] = self._calculate_budget(
results['hiring_plan'],
current_state.get('location', 'US')
)
# Assess risks
results['risk_factors'] = self._assess_scaling_risks(
current_state,
growth_targets
)
# Generate recommendations
results['recommendations'] = self._generate_recommendations(results)
return results
def _analyze_current_state(self, current_state: Dict) -> Dict:
"""Analyze current team state"""
total_engineers = current_state.get('headcount', 0)
analysis = {
'total_headcount': total_engineers,
'team_stage': self._get_team_stage(total_engineers),
'productivity_index': 0,
'balance_score': 0,
'issues': []
}
# Calculate productivity index
if total_engineers > 0:
velocity = current_state.get('velocity', 100)
expected_velocity = total_engineers * 20 # baseline 20 points per engineer
analysis['productivity_index'] = (velocity / expected_velocity) * 100
# Check team balance
roles = current_state.get('roles', {})
analysis['balance_score'] = self._calculate_balance_score(roles, total_engineers)
# Identify issues
if analysis['productivity_index'] < 70:
analysis['issues'].append('Low productivity - possible process or tooling issues')
if analysis['balance_score'] < 60:
analysis['issues'].append('Team imbalance - review role distribution')
manager_ratio = roles.get('managers', 0) / max(total_engineers, 1)
if manager_ratio > 0.2:
analysis['issues'].append('Over-managed - too many managers')
elif manager_ratio < 0.08 and total_engineers > 20:
analysis['issues'].append('Under-managed - need more engineering managers')
return analysis
def _get_team_stage(self, headcount: int) -> str:
"""Determine team stage based on size"""
for stage, config in self.team_structures.items():
if config['min'] <= headcount <= config['max']:
return stage
return 'startup'
def _calculate_balance_score(self, roles: Dict, total: int) -> float:
"""Calculate team balance score"""
if total == 0:
return 0
score = 100
ideal_ratios = self.role_ratios
for role, ideal_ratio in ideal_ratios.items():
actual_count = roles.get(role, 0)
actual_ratio = actual_count / total
# Penalize deviation from ideal ratio
deviation = abs(actual_ratio - ideal_ratio)
penalty = deviation * 100
score -= min(penalty, 20) # Max 20 point penalty per role
return max(0, score)
def _create_growth_timeline(self, current: Dict, targets: Dict) -> List[Dict]:
"""Create quarterly growth timeline"""
current_headcount = current.get('headcount', 0)
target_headcount = targets.get('target_headcount', current_headcount)
timeline_quarters = targets.get('timeline_quarters', 4)
growth_needed = target_headcount - current_headcount
timeline = []
for quarter in range(1, timeline_quarters + 1):
# Apply Brooks' Law - diminishing returns with rapid growth
if quarter == 1:
quarterly_growth = math.ceil(growth_needed * 0.4) # Front-load hiring
else:
remaining_growth = target_headcount - current_headcount
quarters_left = timeline_quarters - quarter + 1
quarterly_growth = math.ceil(remaining_growth / quarters_left)
# Adjust for onboarding capacity
max_onboarding = math.ceil(current_headcount * 0.25) # 25% growth per quarter max
quarterly_growth = min(quarterly_growth, max_onboarding)
current_headcount += quarterly_growth
timeline.append({
'quarter': f'Q{quarter}',
'headcount': current_headcount,
'new_hires': quarterly_growth,
'onboarding_capacity': max_onboarding,
'productivity_factor': 1.0 - (0.2 * (quarterly_growth / max(current_headcount, 1)))
})
return timeline
def _generate_hiring_plan(self, current: Dict, targets: Dict) -> Dict:
"""Generate detailed hiring plan"""
current_roles = current.get('roles', {})
target_headcount = targets.get('target_headcount', 0)
hiring_plan = {
'total_hires_needed': target_headcount - current.get('headcount', 0),
'by_role': {},
'by_quarter': {},
'interview_capacity_needed': 0,
'recruiting_resources': 0
}
# Calculate ideal role distribution
for role, ideal_ratio in self.role_ratios.items():
ideal_count = math.ceil(target_headcount * ideal_ratio)
current_count = current_roles.get(role, 0)
hires_needed = max(0, ideal_count - current_count)
if hires_needed > 0:
hiring_plan['by_role'][role] = {
'current': current_count,
'target': ideal_count,
'hires_needed': hires_needed,
'priority': self._get_role_priority(role, current_roles, target_headcount)
}
# Distribute hires across quarters
timeline = self._create_growth_timeline(current, targets)
for quarter_data in timeline:
quarter = quarter_data['quarter']
hires = quarter_data['new_hires']
hiring_plan['by_quarter'][quarter] = {
'total_hires': hires,
'breakdown': self._distribute_quarterly_hires(hires, hiring_plan['by_role'])
}
# Calculate interview capacity (5 interviews per hire average)
hiring_plan['interview_capacity_needed'] = hiring_plan['total_hires_needed'] * 5
# Calculate recruiting resources (1 recruiter per 50 hires/year)
annual_hires = hiring_plan['total_hires_needed'] * (4 / max(targets.get('timeline_quarters', 4), 1))
hiring_plan['recruiting_resources'] = math.ceil(annual_hires / 50)
return hiring_plan
def _get_role_priority(self, role: str, current_roles: Dict, target_size: int) -> int:
"""Determine hiring priority for a role"""
# Priority based on criticality and current gaps
priorities = {
'engineering_manager': 10 if target_size > 20 else 5,
'tech_lead': 9,
'senior_engineer': 8,
'devops': 7 if current_roles.get('devops', 0) == 0 else 5,
'qa': 6,
'mid_engineer': 5,
'product_manager': 6,
'designer': 5,
'data_engineer': 4,
'junior_engineer': 3
}
return priorities.get(role, 5)
def _distribute_quarterly_hires(self, total_hires: int, role_needs: Dict) -> Dict:
"""Distribute quarterly hires across roles"""
distribution = {}
# Sort roles by priority
sorted_roles = sorted(
role_needs.items(),
key=lambda x: x[1]['priority'],
reverse=True
)
remaining_hires = total_hires
for role, needs in sorted_roles:
if remaining_hires <= 0:
break
hires = min(needs['hires_needed'], max(1, remaining_hires // 3))
distribution[role] = hires
remaining_hires -= hires
return distribution
def _design_team_structure(self, target_headcount: int) -> Dict:
"""Design optimal team structure"""
stage = self._get_team_stage(target_headcount)
structure = {
'organizational_model': self.team_structures[stage]['structure'],
'teams': [],
'reporting_structure': {},
'communication_paths': 0
}
if stage == 'startup':
structure['teams'] = [{
'name': 'Core Team',
'size': target_headcount,
'focus': 'Full-stack'
}]
elif stage == 'growth':
# Create 2-4 teams
team_size = 6
num_teams = math.ceil(target_headcount / team_size)
structure['teams'] = [
{
'name': f'Team {i+1}',
'size': team_size,
'focus': ['Platform', 'Product', 'Infrastructure', 'Growth'][i % 4]
}
for i in range(num_teams)
]
elif stage == 'scale':
# Create departments with multiple teams
structure['departments'] = [
{'name': 'Platform', 'teams': 3, 'headcount': target_headcount * 0.3},
{'name': 'Product', 'teams': 4, 'headcount': target_headcount * 0.4},
{'name': 'Infrastructure', 'teams': 2, 'headcount': target_headcount * 0.2},
{'name': 'Data', 'teams': 1, 'headcount': target_headcount * 0.1}
]
# Calculate communication paths (n*(n-1)/2)
structure['communication_paths'] = (target_headcount * (target_headcount - 1)) // 2
# Add management layers
structure['management_layers'] = math.ceil(math.log(target_headcount, 7))
return structure
def _calculate_budget(self, hiring_plan: Dict, location: str) -> Dict:
"""Calculate budget projection"""
# Average salaries by role and location (in USD)
salary_bands = {
'US': {
'engineering_manager': 200000,
'tech_lead': 180000,
'senior_engineer': 160000,
'mid_engineer': 120000,
'junior_engineer': 85000,
'devops': 150000,
'qa': 100000,
'product_manager': 150000,
'designer': 120000,
'data_engineer': 140000
},
'EU': {
'engineering_manager': 160000,
'tech_lead': 144000,
'senior_engineer': 128000,
'mid_engineer': 96000,
'junior_engineer': 68000,
'devops': 120000,
'qa': 80000,
'product_manager': 120000,
'designer': 96000,
'data_engineer': 112000
},
'APAC': {
'engineering_manager': 120000,
'tech_lead': 108000,
'senior_engineer': 96000,
'mid_engineer': 72000,
'junior_engineer': 51000,
'devops': 90000,
'qa': 60000,
'product_manager': 90000,
'designer': 72000,
'data_engineer': 84000
}
}
location_salaries = salary_bands.get(location, salary_bands['US'])
budget = {
'annual_salary_cost': 0,
'benefits_cost': 0, # 30% of salary
'equipment_cost': 0, # $5k per hire
'recruiting_cost': 0, # 20% of first-year salary
'onboarding_cost': 0, # $10k per hire
'total_cost': 0,
'cost_per_hire': 0
}
for role, details in hiring_plan['by_role'].items():
hires = details['hires_needed']
salary = location_salaries.get(role, 100000)
budget['annual_salary_cost'] += hires * salary
budget['recruiting_cost'] += hires * salary * 0.2
budget['benefits_cost'] = budget['annual_salary_cost'] * 0.3
budget['equipment_cost'] = hiring_plan['total_hires_needed'] * 5000
budget['onboarding_cost'] = hiring_plan['total_hires_needed'] * 10000
budget['total_cost'] = sum([
budget['annual_salary_cost'],
budget['benefits_cost'],
budget['equipment_cost'],
budget['recruiting_cost'],
budget['onboarding_cost']
])
if hiring_plan['total_hires_needed'] > 0:
budget['cost_per_hire'] = budget['total_cost'] / hiring_plan['total_hires_needed']
return budget
def _assess_scaling_risks(self, current: Dict, targets: Dict) -> List[Dict]:
"""Assess risks in scaling plan"""
risks = []
growth_rate = (targets['target_headcount'] - current['headcount']) / max(current['headcount'], 1)
if growth_rate > 1.0: # More than 100% growth
risks.append({
'risk': 'Rapid growth dilution',
'impact': 'High',
'mitigation': 'Implement strong onboarding and mentorship programs'
})
if current.get('attrition_rate', 0) > 15:
risks.append({
'risk': 'High attrition during scaling',
'impact': 'High',
'mitigation': 'Address retention issues before aggressive hiring'
})
if targets.get('timeline_quarters', 4) < 4:
risks.append({
'risk': 'Compressed timeline',
'impact': 'Medium',
'mitigation': 'Consider extending timeline or increasing recruiting resources'
})
return risks
def _generate_recommendations(self, results: Dict) -> List[str]:
"""Generate scaling recommendations"""
recommendations = []
# Based on growth rate
total_hires = results['hiring_plan']['total_hires_needed']
current_size = results['current_analysis']['total_headcount']
if current_size > 0:
growth_rate = total_hires / current_size
if growth_rate > 0.5:
recommendations.append('Consider hiring a dedicated recruiting team')
recommendations.append('Implement scalable onboarding processes')
recommendations.append('Establish clear team charters and boundaries')
if growth_rate > 1.0:
recommendations.append('⚠️ High growth risk - consider slowing timeline')
recommendations.append('Focus on senior hires first to establish culture')
recommendations.append('Implement continuous integration practices early')
# Based on structure
if results['team_structure']['communication_paths'] > 1000:
recommendations.append('Implement clear communication channels and tools')
recommendations.append('Consider platform teams to reduce dependencies')
# Based on balance
if results['current_analysis']['balance_score'] < 70:
recommendations.append('Prioritize hiring for underrepresented roles')
recommendations.append('Consider role rotation for skill development')
return recommendations
def calculate_team_scaling(current_state: Dict, growth_targets: Dict) -> str:
"""Main function to calculate team scaling"""
calculator = TeamScalingCalculator()
results = calculator.calculate_scaling_plan(current_state, growth_targets)
# Format output
output = [
"=== Engineering Team Scaling Plan ===",
f"",
f"Current State Analysis:",
f" Current Headcount: {results['current_analysis']['total_headcount']}",
f" Team Stage: {results['current_analysis']['team_stage']}",
f" Productivity Index: {results['current_analysis']['productivity_index']:.1f}%",
f" Team Balance Score: {results['current_analysis']['balance_score']:.1f}/100",
f"",
f"Growth Plan:",
f" Target Headcount: {growth_targets['target_headcount']}",
f" Total Hires Needed: {results['hiring_plan']['total_hires_needed']}",
f" Timeline: {growth_targets['timeline_quarters']} quarters",
f"",
"Quarterly Timeline:"
]
for quarter in results['growth_timeline']:
output.append(
f" {quarter['quarter']}: {quarter['headcount']} total "
f"(+{quarter['new_hires']} hires, "
f"{quarter['productivity_factor']:.0%} productivity)"
)
output.extend([
f"",
"Hiring Priorities:"
])
sorted_roles = sorted(
results['hiring_plan']['by_role'].items(),
key=lambda x: x[1]['priority'],
reverse=True
)
for role, details in sorted_roles[:5]:
output.append(
f" {role}: {details['hires_needed']} hires "
f"(Priority: {details['priority']}/10)"
)
output.extend([
f"",
f"Budget Projection:",
f" Annual Salary Cost: ,.0f",
f" Total Investment: ,.0f",
f" Cost per Hire: ,.0f",
f"",
f"Team Structure:",
f" Model: {results['team_structure']['organizational_model']}",
f" Management Layers: {results['team_structure']['management_layers']}",
f" Communication Paths: {results['team_structure']['communication_paths']:,}",
f"",
"Key Recommendations:"
])
for rec in results['recommendations']:
output.append(f" • {rec}")
return '\n'.join(output)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="Engineering Team Scaling Calculator - Optimize team growth and structure"
)
parser.add_argument(
"input_file", nargs="?", default=None,
help="JSON file with current_state and growth_targets (default: run with sample data)"
)
parser.add_argument(
"--json", action="store_true",
help="Output raw JSON instead of formatted report"
)
args = parser.parse_args()
if args.input_file:
with open(args.input_file) as f:
data = json.load(f)
current_state = data["current_state"]
growth_targets = data["growth_targets"]
else:
current_state = {
'headcount': 25,
'velocity': 450,
'roles': {
'engineering_manager': 2,
'tech_lead': 3,
'senior_engineer': 8,
'mid_engineer': 10,
'junior_engineer': 2
},
'attrition_rate': 12,
'location': 'US'
}
growth_targets = {
'target_headcount': 75,
'timeline_quarters': 4
}
if args.json:
calculator = TeamScalingCalculator()
results = calculator.calculate_scaling_plan(current_state, growth_targets)
print(json.dumps(results, indent=2))
else:
print(calculate_team_scaling(current_state, growth_targets))
FILE:cto-advisor/scripts/tech_debt_analyzer.py
#!/usr/bin/env python3
"""
Technical Debt Analyzer - Assess and prioritize technical debt across systems
"""
import json
from typing import Dict, List, Tuple
from datetime import datetime
import math
class TechDebtAnalyzer:
def __init__(self):
self.debt_categories = {
'architecture': {
'weight': 0.25,
'indicators': [
'monolithic_design', 'tight_coupling', 'no_microservices',
'legacy_patterns', 'no_api_gateway', 'synchronous_only'
]
},
'code_quality': {
'weight': 0.20,
'indicators': [
'low_test_coverage', 'high_complexity', 'code_duplication',
'no_documentation', 'inconsistent_standards', 'legacy_language'
]
},
'infrastructure': {
'weight': 0.20,
'indicators': [
'manual_deployments', 'no_ci_cd', 'single_points_failure',
'no_monitoring', 'no_auto_scaling', 'outdated_servers'
]
},
'security': {
'weight': 0.20,
'indicators': [
'outdated_dependencies', 'no_security_scans', 'plain_text_secrets',
'no_encryption', 'missing_auth', 'no_audit_logs'
]
},
'performance': {
'weight': 0.15,
'indicators': [
'slow_response_times', 'no_caching', 'inefficient_queries',
'memory_leaks', 'no_optimization', 'blocking_operations'
]
}
}
self.impact_matrix = {
'user_impact': {'weight': 0.30, 'score': 0},
'developer_velocity': {'weight': 0.25, 'score': 0},
'system_reliability': {'weight': 0.20, 'score': 0},
'scalability': {'weight': 0.15, 'score': 0},
'maintenance_cost': {'weight': 0.10, 'score': 0}
}
def analyze_system(self, system_data: Dict) -> Dict:
"""Analyze a system for technical debt"""
results = {
'timestamp': datetime.now().isoformat(),
'system_name': system_data.get('name', 'Unknown'),
'debt_score': 0,
'debt_level': '',
'category_scores': {},
'prioritized_actions': [],
'estimated_effort': {},
'risk_assessment': {},
'recommendations': []
}
# Calculate debt scores by category
total_debt_score = 0
for category, config in self.debt_categories.items():
category_score = self._calculate_category_score(
system_data.get(category, {}),
config['indicators']
)
weighted_score = category_score * config['weight']
results['category_scores'][category] = {
'raw_score': category_score,
'weighted_score': weighted_score,
'level': self._get_level(category_score)
}
total_debt_score += weighted_score
results['debt_score'] = round(total_debt_score, 2)
results['debt_level'] = self._get_level(total_debt_score)
# Calculate impact and prioritize
results['prioritized_actions'] = self._prioritize_actions(
results['category_scores'],
system_data.get('business_context', {})
)
# Estimate effort
results['estimated_effort'] = self._estimate_effort(
results['prioritized_actions'],
system_data.get('team_size', 5)
)
# Risk assessment
results['risk_assessment'] = self._assess_risks(
results['debt_score'],
system_data.get('system_criticality', 'medium')
)
# Generate recommendations
results['recommendations'] = self._generate_recommendations(results)
return results
def _calculate_category_score(self, category_data: Dict, indicators: List) -> float:
"""Calculate score for a specific category"""
if not category_data:
return 50.0 # Default middle score if no data
total_score = 0
count = 0
for indicator in indicators:
if indicator in category_data:
# Score from 0 (no debt) to 100 (high debt)
total_score += category_data[indicator]
count += 1
return (total_score / count) if count > 0 else 50.0
def _get_level(self, score: float) -> str:
"""Convert numerical score to level"""
if score < 20:
return 'Low'
elif score < 40:
return 'Medium-Low'
elif score < 60:
return 'Medium'
elif score < 80:
return 'Medium-High'
else:
return 'Critical'
def _prioritize_actions(self, category_scores: Dict, business_context: Dict) -> List:
"""Prioritize technical debt reduction actions"""
actions = []
for category, scores in category_scores.items():
if scores['raw_score'] > 60: # Focus on high debt areas
priority = self._calculate_priority(
scores['raw_score'],
category,
business_context
)
action = {
'category': category,
'priority': priority,
'score': scores['raw_score'],
'action_items': self._get_action_items(category, scores['level'])
}
actions.append(action)
# Sort by priority
actions.sort(key=lambda x: x['priority'], reverse=True)
return actions[:5] # Top 5 priorities
def _calculate_priority(self, score: float, category: str, context: Dict) -> float:
"""Calculate priority based on score and business context"""
base_priority = score
# Adjust based on business context
if context.get('growth_phase') == 'rapid' and category in ['scalability', 'performance']:
base_priority *= 1.5
if context.get('compliance_required') and category == 'security':
base_priority *= 2.0
if context.get('cost_pressure') and category == 'infrastructure':
base_priority *= 1.3
return min(100, base_priority)
def _get_action_items(self, category: str, level: str) -> List[str]:
"""Get specific action items based on category and level"""
actions = {
'architecture': {
'Critical': [
'Immediate: Create architecture migration roadmap',
'Week 1: Identify service boundaries for decomposition',
'Month 1: Begin extracting first microservice',
'Month 2: Implement API gateway',
'Quarter: Complete critical service separation'
],
'Medium-High': [
'Month 1: Document current architecture',
'Month 2: Design target architecture',
'Quarter: Begin gradual migration',
'Monitor: Track coupling metrics'
]
},
'code_quality': {
'Critical': [
'Immediate: Implement code quality gates',
'Week 1: Set up automated testing pipeline',
'Month 1: Achieve 40% test coverage',
'Month 2: Refactor critical modules',
'Quarter: Reach 70% test coverage'
],
'Medium-High': [
'Month 1: Establish coding standards',
'Month 2: Implement code review process',
'Quarter: Gradual refactoring plan'
]
},
'infrastructure': {
'Critical': [
'Immediate: Implement basic CI/CD',
'Week 1: Set up monitoring and alerts',
'Month 1: Automate critical deployments',
'Month 2: Implement disaster recovery',
'Quarter: Full infrastructure as code'
],
'Medium-High': [
'Month 1: Document infrastructure',
'Month 2: Begin automation',
'Quarter: Modernize critical components'
]
},
'security': {
'Critical': [
'Immediate: Security audit and patching',
'Week 1: Implement secrets management',
'Month 1: Set up vulnerability scanning',
'Month 2: Implement security training',
'Quarter: Achieve compliance standards'
],
'Medium-High': [
'Month 1: Security assessment',
'Month 2: Implement security tools',
'Quarter: Regular security reviews'
]
},
'performance': {
'Critical': [
'Immediate: Performance profiling',
'Week 1: Implement caching strategy',
'Month 1: Optimize database queries',
'Month 2: Implement CDN',
'Quarter: Re-architect bottlenecks'
],
'Medium-High': [
'Month 1: Performance baseline',
'Month 2: Optimization plan',
'Quarter: Incremental improvements'
]
}
}
return actions.get(category, {}).get(level, ['Create action plan'])
def _estimate_effort(self, actions: List, team_size: int) -> Dict:
"""Estimate effort required for debt reduction"""
total_story_points = 0
effort_breakdown = {}
for action in actions:
# Estimate based on category and score
base_points = action['score'] * 2 # Higher debt = more effort
if action['category'] == 'architecture':
points = base_points * 1.5 # Architecture changes are complex
elif action['category'] == 'security':
points = base_points * 1.2 # Security requires careful work
else:
points = base_points
effort_breakdown[action['category']] = {
'story_points': round(points),
'sprints': math.ceil(points / (team_size * 20)), # 20 points per dev per sprint
'developers_needed': math.ceil(points / 100)
}
total_story_points += points
return {
'total_story_points': round(total_story_points),
'estimated_sprints': math.ceil(total_story_points / (team_size * 20)),
'recommended_team_size': max(team_size, math.ceil(total_story_points / 200)),
'breakdown': effort_breakdown
}
def _assess_risks(self, debt_score: float, criticality: str) -> Dict:
"""Assess risks associated with technical debt"""
risk_level = 'Low'
if debt_score > 70 and criticality == 'high':
risk_level = 'Critical'
elif debt_score > 60 or criticality == 'high':
risk_level = 'High'
elif debt_score > 40:
risk_level = 'Medium'
risks = {
'overall_risk': risk_level,
'specific_risks': []
}
if debt_score > 60:
risks['specific_risks'].extend([
'System failure risk increasing',
'Developer productivity declining',
'Innovation velocity blocked',
'Maintenance costs escalating'
])
if debt_score > 80:
risks['specific_risks'].extend([
'Competitive disadvantage emerging',
'Talent retention risk',
'Customer satisfaction impact',
'Potential data breach vulnerability'
])
return risks
def _generate_recommendations(self, results: Dict) -> List[str]:
"""Generate strategic recommendations"""
recommendations = []
# Overall strategy based on debt level
if results['debt_level'] == 'Critical':
recommendations.append('🚨 URGENT: Dedicate 40% of engineering capacity to debt reduction')
recommendations.append('Create dedicated debt reduction team')
recommendations.append('Implement weekly debt reduction reviews')
recommendations.append('Consider temporary feature freeze')
elif results['debt_level'] in ['Medium-High', 'High']:
recommendations.append('Allocate 25-30% of sprints to debt reduction')
recommendations.append('Establish technical debt budget')
recommendations.append('Implement debt prevention practices')
else:
recommendations.append('Maintain 15-20% ongoing debt reduction allocation')
recommendations.append('Focus on prevention over correction')
# Category-specific recommendations
for category, scores in results['category_scores'].items():
if scores['raw_score'] > 70:
if category == 'architecture':
recommendations.append(f'Consider hiring architecture specialist')
elif category == 'security':
recommendations.append(f'Engage security audit firm')
elif category == 'performance':
recommendations.append(f'Implement performance SLA monitoring')
# Team recommendations
effort = results.get('estimated_effort', {})
if effort.get('recommended_team_size', 0) > effort.get('total_story_points', 0) / 200:
recommendations.append(f"Scale team to {effort['recommended_team_size']} engineers")
return recommendations
def analyze_technical_debt(system_config: Dict) -> str:
"""Main function to analyze technical debt"""
analyzer = TechDebtAnalyzer()
results = analyzer.analyze_system(system_config)
# Format output
output = [
f"=== Technical Debt Analysis Report ===",
f"System: {results['system_name']}",
f"Analysis Date: {results['timestamp'][:10]}",
f"",
f"OVERALL DEBT SCORE: {results['debt_score']}/100 ({results['debt_level']})",
f"",
"Category Breakdown:"
]
for category, scores in results['category_scores'].items():
output.append(f" {category.title()}: {scores['raw_score']:.1f} ({scores['level']})")
output.extend([
f"",
"Risk Assessment:",
f" Overall Risk: {results['risk_assessment']['overall_risk']}"
])
for risk in results['risk_assessment']['specific_risks']:
output.append(f" • {risk}")
output.extend([
f"",
"Effort Estimation:",
f" Total Story Points: {results['estimated_effort']['total_story_points']}",
f" Estimated Sprints: {results['estimated_effort']['estimated_sprints']}",
f" Recommended Team Size: {results['estimated_effort']['recommended_team_size']}",
f"",
"Top Priority Actions:"
])
for i, action in enumerate(results['prioritized_actions'][:3], 1):
output.append(f"\n{i}. {action['category'].title()} (Priority: {action['priority']:.0f})")
for item in action['action_items'][:3]:
output.append(f" - {item}")
output.extend([
f"",
"Strategic Recommendations:"
])
for rec in results['recommendations']:
output.append(f" • {rec}")
return '\n'.join(output)
if __name__ == "__main__":
# Example usage
example_system = {
'name': 'Legacy E-commerce Platform',
'architecture': {
'monolithic_design': 80,
'tight_coupling': 70,
'no_microservices': 90,
'legacy_patterns': 60
},
'code_quality': {
'low_test_coverage': 75,
'high_complexity': 65,
'code_duplication': 55
},
'infrastructure': {
'manual_deployments': 70,
'no_ci_cd': 60,
'no_monitoring': 40
},
'security': {
'outdated_dependencies': 85,
'no_security_scans': 70
},
'performance': {
'slow_response_times': 60,
'no_caching': 50
},
'team_size': 8,
'system_criticality': 'high',
'business_context': {
'growth_phase': 'rapid',
'compliance_required': True,
'cost_pressure': False
}
}
print(analyze_technical_debt(example_system))
FILE:culture-architect/SKILL.md
---
name: "culture-architect"
description: "Build, measure, and evolve company culture as operational behavior — not wall posters. Covers mission/vision/values workshops, values-to-behaviors translation, culture code creation, culture health assessment, and cultural rituals by stage. Use when building company values, assessing culture health, designing cultural rituals, creating culture codes, handling culture clashes, or when user mentions culture, values, culture debt, founder culture, or culture code."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: culture-leadership
updated: 2026-03-05
frameworks: culture-playbook, culture-code-template
---
# Culture Architect
Culture is what you DO, not what you SAY. This skill builds culture as an operational system — observable behaviors, measurable health, and rituals that scale.
## Keywords
culture, company culture, values, mission, vision, culture code, cultural rituals, culture health, values-to-behaviors, founder culture, culture debt, value-washing, culture assessment, culture survey, Netflix culture deck, HubSpot culture code, psychological safety, culture scaling
## Core Principle
**Culture = (What you reward) + (What you tolerate) + (What you celebrate)**
If your values say "transparency" but you punish bearers of bad news — your real value is "optics." Culture is not aspirational. It's descriptive. The work is closing the gap between stated and actual.
## Frameworks
### 1. Mission / Vision / Values Workshop
Run this conversationally, not as a corporate offsite. Three questions:
**Mission** — Why do we exist (beyond making money)?
- "What would be lost if we disappeared tomorrow?"
- Mission is present-tense. "We reduce preventable falls in elderly care." Not "to be the leading..."
**Vision** — What does winning look like in 5–10 years?
- Specific enough to be wrong. "Every care home in Europe uses our system" beats "be the market leader."
**Values** — What behaviors do we actually model?
- Start with what you observe, not what sounds good. "What did our last great hire do that nobody asked them to?"
- Keep to 3–5. More than 5 and none of them mean anything.
### 2. Values → Behaviors Translation
This is the work. Every value needs behavioral anchors or it's decoration.
| Value | Bad version | Behavioral anchor |
|-------|------------|-------------------|
| Transparency | "We're open and honest" | "We share bad news within 24 hours, including to our manager" |
| Ownership | "We take responsibility" | "We don't hand off problems — we own them until resolved, even across team boundaries" |
| Speed | "We move fast" | "Decisions under €5K happen at team level, same day, no approval needed" |
| Quality | "We don't cut corners" | "We stop the line before shipping something we're not proud of" |
| Customer-first | "Customers are our priority" | "Any team member can escalate a customer issue to leadership, bypassing normal channels" |
**Workshop exercise:** Write your value. Then ask "How would a new hire know we actually live this on day 30?" If you can't answer concretely, it's not a value — it's an aspiration.
### 3. Culture Code Creation
A culture code is a public document that describes how you operate. It should scare off the wrong people and attract the right ones.
**Structure:**
1. Who we are (mission + context)
2. Who thrives here (specific behaviors, not adjectives)
3. Who doesn't thrive here (honest — this is the useful part)
4. How we make decisions
5. How we communicate
6. How we grow people
7. What we expect of leaders
See `templates/culture-code-template.md` for a complete template.
**Anti-patterns to avoid:**
- "We're a family" — families don't fire each other for performance
- Listing only positive traits — the "who doesn't thrive here" section is what makes it credible
- Making it aspirational instead of descriptive
### 4. Culture Health Assessment
Run quarterly. 8–12 questions. Anonymous. See `references/culture-playbook.md` for survey design.
**Core areas to measure:**
1. Psychological safety — "Can I raise a concern without fear?"
2. Clarity — "Do I know how my work connects to company goals?"
3. Fairness — "Are decisions made consistently and transparently?"
4. Growth — "Am I learning and being challenged here?"
5. Trust in leadership — "Do I believe what leadership tells me?"
**Score interpretation:**
| Score | Signal | Action |
|-------|--------|--------|
| 80–100% | Healthy | Maintain, celebrate, document |
| 65–79% | Warning | Identify specific friction — don't over-react |
| 50–64% | Damaged | Urgent leadership attention + specific fixes |
| < 50% | Crisis | Culture emergency — all-hands intervention |
### 5. Cultural Rituals by Stage
Rituals are the delivery mechanism for culture. What works at 10 people breaks at 100.
**Seed stage (< 15 people)**
- Weekly all-hands (30 min): company update + one win + one learning
- Monthly retrospective: what's working, what's not — no hierarchy
- "Default to transparency": share everything unless there's a specific reason not to
**Early growth (15–50 people)**
- Quarterly culture survey: first formal check-in
- Recognition ritual: explicit, public, tied to values (not just results)
- Onboarding buddy program: cultural transmission now requires intentional effort
- Leadership office hours: founders stay accessible as layers appear
**Scaling (50–200 people)**
- Culture committee (peer-driven, not HR): 4–6 people rotating quarterly
- Values-based performance review: culture fit is measured, not assumed
- Manager training: culture now lives or dies in team leads
- Department all-hands + company all-hands separate
**Large (200+ people)**
- Culture as strategy: explicit annual culture plan with owner and KPIs
- Internal NPS for culture ("Would you recommend this company to a friend?")
- Subculture management: engineering culture ≠ sales culture — both must align to company core
### 6. Culture Anti-Patterns
**Value-washing:** Listing values you don't practice. Symptom: employees roll their eyes during values discussions.
- Fix: Run a values audit. Ask "What did the last person who got promoted demonstrate?" If it doesn't match your values, your real values are different.
**Culture debt:** Accumulating cultural compromises over time. "We'll address the toxic star performer later." Later compounds.
- Fix: Act on culture violations faster than you think necessary. One tolerated bad behavior destroys what ten good behaviors build.
**Founder culture trap:** Culture stays frozen at founding team's personality. New hires assimilate or leave.
- Fix: Explicitly evolve values as you scale. What worked at 10 people (move fast, ask forgiveness) may be destructive at 100 (we need process).
**Culture by osmosis:** Assuming culture transmits naturally. It did at 10 people. It doesn't at 50.
- Fix: Make culture intentional. Document it. Teach it. Measure it. Reward it explicitly.
## Culture Integration with C-Suite
| When... | Culture Architect works with... | To... |
|---------|---------------------------------|-------|
| Hiring surge | CHRO | Ensure culture fit is measured, not guessed |
| Org reorg | COO + CEO | Manage culture disruption from structure change |
| M&A or partnership | CEO + COO | Detect and resolve culture clashes early |
| Performance issues | CHRO | Separate culture fit from skill deficit |
| Strategy pivot | CEO | Update values/behaviors that the pivot makes obsolete |
| Rapid growth | All | Scale rituals before culture dilutes |
## Key Questions a Culture Architect Asks
- "Can you name the last person we fired for culture reasons? What did they do?"
- "What behavior got your last promoted employee promoted? Is that in your values?"
- "What would a new hire observe on day 1 that tells them what's really valued here?"
- "What do we tolerate that we shouldn't? Who knows and does nothing?"
- "How does a team lead in Berlin know what the culture is in Madrid?"
## Red Flags
- Values posted on the wall, never referenced in reviews or decisions
- Star performers protected from cultural standards
- Leaders who "don't have time" for culture rituals
- New hires feeling the culture is "different than advertised"
- No mechanism to raise cultural concerns safely
- Culture survey results never shared with the team
## Detailed References
- `references/culture-playbook.md` — Netflix analysis, survey design, ritual examples, M&A playbook
- `templates/culture-code-template.md` — Culture code document template
FILE:culture-architect/references/culture-playbook.md
# Culture Playbook
Reference frameworks for building, measuring, and evolving company culture.
---
## 1. Netflix Culture Deck — What Works, What Doesn't
Reed Hastings published this in 2009. 125 slides. 20M+ views. It changed how tech companies think about culture.
### What works
**"Adequate performance gets a generous severance"** — This is the sentence that made HR professionals uncomfortable. It's also why Netflix has high performers. If you keep B-players, A-players leave.
**Context, not control** — Instead of rules and approvals, Netflix provides context (strategy, goals, constraints) and expects people to make good decisions. This only works if you actually hire people who can.
**"Freedom and responsibility" as a pair** — You can't have one without the other. Freedom without responsibility is chaos. Responsibility without freedom is bureaucracy.
**Publicly stated values actually describe behavior** — The deck is descriptive, not aspirational. It says "here's what we actually do." That's rare and valuable.
### What doesn't work (or doesn't transfer)
**"We are not a family"** — Works at Netflix, lands badly in many cultures (especially European). The principle underneath it is valid: performance matters. The framing is optional.
**"Keeper test"** — "Would I fight to keep this person?" Powerful tool, but managers need coaching to use it well. Without context, it becomes paranoia-inducing.
**No vacation policy** — Works when managers model healthy vacation use. Doesn't work when culture implicitly punishes taking time off. The policy is neutral; the culture around it determines the outcome.
**Radical transparency on compensation** — Netflix publishes pay bands. This works in high-trust, high-fairness environments. In environments with existing pay inequities, it creates problems before it fixes them.
### Key lesson
The Netflix culture deck works because it's honest about tradeoffs. Your culture code should be equally honest. "We move fast, which sometimes means decisions get revisited" is more credible than "we move fast AND we get it right the first time."
---
## 2. Values-to-Behaviors Mapping Framework
Values without behavioral anchors are intentions. Behavioral anchors make values operational.
### The mapping process
**Step 1: List your stated values**
Don't curate. Write down everything on the values list, however it's currently stated.
**Step 2: For each value, find three real examples**
"Describe a time in the last 6 months when someone exemplified [value]."
If you can't find three examples, the value isn't real.
**Step 3: Extract the observable behavior**
From the examples, identify the specific action. Not the feeling, not the intention — the action.
**Step 4: Write the behavioral anchor**
Format: "[Subject] does [specific action] in [specific context]."
**Step 5: Find the counter-example**
For each value, identify a behavior that violates it. This is what you don't tolerate.
Format: "[Subject] does NOT [specific opposite action] even when [temptation/pressure]."
### Example mapping: "Customer Obsession"
| Component | Content |
|-----------|---------|
| Value | Customer Obsession |
| Example 1 | PM delayed a sprint to fix a bug a customer reported on a call, even though it wasn't on the roadmap |
| Example 2 | Support rep escalated a technical issue directly to engineering at 9pm, resolved within 2 hours |
| Example 3 | Sales declined a deal that would have required features that would hurt existing customers |
| Behavioral anchor | "We resolve customer-reported critical issues within 24 hours, regardless of roadmap priority" |
| Counter-example | "We do not close a customer issue as 'resolved' until the customer confirms it's resolved" |
### Common mapping mistakes
**Too vague:** "We put customers first" — this doesn't change behavior.
**Too broad:** "We care about quality in everything we do" — can't be measured or violated.
**Too personal:** "We're passionate" — describes emotion, not action.
**Too aspirational:** "We strive to deliver world-class..." — "strive" lets you off the hook.
---
## 3. Culture Survey Design — 8-12 Questions That Reveal Truth
Most culture surveys are useless because they measure satisfaction, not health. Satisfaction can be high in a dysfunctional culture ("I like my team, my boss, my pay" ≠ healthy culture).
### Survey design principles
1. **Anonymous, always.** If it's not anonymous, people answer what they think you want to hear.
2. **Short enough to complete honestly.** 8–12 questions max. 15 minutes max.
3. **Likert + open text.** "On a scale of 1–5" captures signal. "Why did you give that score?" captures insight.
4. **Action-linked.** Never run a survey unless you're prepared to share results and act on them.
5. **Consistent questions over time.** You want trend data, not one-off snapshots.
### The 10-question core survey
| # | Question | Area measured |
|---|----------|---------------|
| 1 | I can raise concerns or disagreements with my manager without fear of negative consequences. | Psychological safety |
| 2 | I know how my work connects to the company's most important goals. | Clarity/alignment |
| 3 | When I make a mistake, I can be honest about it without hiding it. | Psychological safety |
| 4 | Decisions here are made based on merit and data, not politics or relationships. | Fairness |
| 5 | I trust that leadership tells us the truth, even when it's bad news. | Trust in leadership |
| 6 | I am growing and being challenged in my current role. | Growth |
| 7 | When someone underperforms and nothing happens, I feel that's handled appropriately. | Accountability |
| 8 | I feel comfortable being myself at work. | Inclusion |
| 9 | My manager recognizes my contributions in ways that feel meaningful. | Recognition |
| 10 | I would recommend this company as a great place to work to someone I respect. | Overall health (eNPS) |
### Follow-up open text questions (pick 2–3)
- "What's the one thing leadership could do differently that would most improve the culture?"
- "What do we tolerate that we shouldn't?"
- "What should we protect as we grow that we're at risk of losing?"
- "What's the gap between what we say we value and what we actually do?"
### Analyzing results
**eNPS (question 10):** Score = % Promoters (9–10) minus % Detractors (1–6). Healthy: > 20. Great: > 40.
**Questions 1 and 3 (psychological safety):** If below 70%, you have a leadership problem, not a culture problem. Fix the manager first.
**Question 7 (accountability):** This is the most honest question. Cultures that fail to hold underperformers accountable destroy high-performer retention.
**Biggest drop between surveys:** This is your fire. Don't average it away.
---
## 4. Cultural Ritual Examples by Company Stage
### Seed (< 15 people)
**Weekly "Wins and Learnings" (15 min, Fridays)**
- Each person shares one win (however small) and one learning (failure, insight, mistake)
- No slides. No prep. Just talking.
- Purpose: normalizes imperfection, builds psychological safety early
**"Open book" financials**
- Share revenue, burn, runway with the whole team monthly
- Builds owners, not employees
- Requires trust that people won't misuse the data
**"Postmortem as celebration"**
- When something goes wrong, celebrate the post-mortem publicly
- "We learned X, here's how we'll do it differently"
- Prevents a blame culture from forming early
### Early growth (15–50 people)
**Monthly "Founder's Letter"**
- CEO writes an unfiltered update: what we're winning, what's hard, what's changed
- Not polished. Not PR. Real.
- Distributed internally before it goes external
**Values spotlight in team meetings**
- One agenda item: "Who exemplified [value] this week? What did they do?"
- Takes 3 minutes. Trains the muscle for values-linked recognition.
**New hire "30-day truth sessions"**
- At day 30, every new hire meets with a senior leader (not their manager) and answers: "What surprised you? What's different from what you expected? What would you fix?"
- Captures culture signal while the new hire's eyes are still fresh
### Scaling (50–200 people)
**Quarterly culture review**
- Culture committee reviews survey results, names top issues, proposes 2–3 concrete actions
- Results shared with all-hands within 2 weeks of survey close
- 30-day action accountability check-in
**Manager calibration on culture fit**
- Quarterly: managers share one team member who exemplifies culture, one who struggles
- Group discussion on patterns, not individuals
- Identifies culture outliers early before they become retention or performance crises
**"Culture at the edges" audit**
- Review last 10 performance issues, 10 terminations, 10 promotions
- Ask: "Is the pattern consistent with our stated values?"
- This is the reality check. The data doesn't lie.
### Large (200+ people)
**Subculture alignment mapping**
- Each department articulates its micro-culture
- Cross-reference with company core values
- Identify deviations: healthy variation vs. value violation
**Culture ambassador program**
- Peer-nominated, rotating, not HR
- Run culture rituals, surface issues, connect remote/distributed teams
- Budget: small (recognition, team events), influence: large
---
## 5. How to Evolve Culture Without Losing Identity
Culture must evolve as you scale. The mistake is either: (a) refusing to evolve, preserving founder culture that doesn't scale, or (b) evolving so fast that original identity is lost.
### The evolution framework
**Preserve:** Core values that define who you are. These should be stable across stages. If "move fast" is core, it doesn't go away — but its expression changes.
**Adapt:** Behaviors that worked at one stage but need updating. "Move fast" at 10 people = decide same day. At 200 people = decide within 1 week with the right people in the room.
**Add:** New behaviors required at the new scale. "Documentation culture" wasn't needed at 10. It's essential at 100.
**Retire:** Behaviors that actively hurt at scale. "Ask forgiveness, not permission" works at seed. Creates coordination chaos at Series B.
### The evolution process
1. Annual values review (not a rewrite — an audit)
2. Ask: "Which of our current behaviors are we proud of? Which embarrass us?"
3. Identify behaviors to add/adapt/retire
4. Communicate the evolution explicitly: "Here's what's changing and why"
5. Update the culture code, onboarding, and performance criteria
### Communication of culture change
Never let culture evolution look like hypocrisy. Proactively name it:
"We used to make all decisions quickly at the team level. As we've grown, that's created coordination problems. Here's how we're updating that: [new behavior]. The underlying value — speed — hasn't changed. How we deliver it has."
---
## 6. Handling Culture Clashes in M&A or Rapid Hiring
### M&A culture integration
**Before signing:**
- Culture due diligence is as important as financial DD
- Questions to answer: How do they make decisions? What gets people fired? What gets them promoted? What do they celebrate?
- Red flag: "We have a great culture" with no supporting evidence
**First 90 days:**
- Don't impose culture; conduct a bilateral audit
- Identify: what do they do that we should adopt? What do we do that they should adopt? What conflicts must be resolved?
- Assign an integration lead on each side. Give them actual authority.
**Failure mode:** Assuming acquisition = cultural absorption. The target's culture doesn't disappear. It goes underground and resurfaces as dysfunction.
### Rapid hiring culture dilution
When a company doubles in headcount in 12 months, culture dilution is near-certain. Prevention:
1. **Codify before you scale.** Document the culture before the surge, not after.
2. **Onboarding is cultural transmission.** Not just process, not just paperwork — immersion in how decisions get made, what's celebrated, what's not tolerated.
3. **Hire for culture adds, not fits.** "Fit" means homogeneity. "Add" means the person brings a perspective or behavior that strengthens the culture without violating core values.
4. **Manager density matters.** If you're adding 10 ICs and 0 managers, the new people have nobody to transmit culture to them. Hire managers ahead of the curve.
5. **Culture buddy system.** Pair new hires with culture exemplars for the first 60 days.
FILE:culture-architect/templates/culture-code-template.md
# [Company Name] Culture Code
> This document describes how we work, what we value, and what it's like to be here. It's meant to be honest — which means it will attract some people and repel others. Both outcomes are correct.
---
## Who We Are
[2–3 sentences: what you do, who you serve, what would be lost if you disappeared.]
**Our mission:** [One sentence. Present tense. Specific enough to be wrong.]
**Our vision:** [Where we'll be in 5–10 years. Specific enough to debate.]
---
## What We Value
*Values are behaviors, not adjectives. Each one has a "this is what it looks like" and a "this is what it doesn't look like."*
### [Value 1]
**What this means:** [Behavioral anchor — what someone does when they live this value]
**What this doesn't mean:** [The misconception or violation to guard against]
**Example:** [A real story of this value in action at your company]
---
### [Value 2]
**What this means:** [Behavioral anchor]
**What this doesn't mean:** [The misconception or violation]
**Example:** [Real story]
---
### [Value 3]
**What this means:** [Behavioral anchor]
**What this doesn't mean:** [The misconception or violation]
**Example:** [Real story]
---
*(Repeat for each value. 3–5 total. Never more than 5.)*
---
## Who Thrives Here
*These are specific, observable behaviors — not personality traits or adjectives.*
- You raise problems early, not after they've grown. You don't complain privately and stay silent publicly.
- You own decisions even when the outcome isn't what you expected.
- You say "I don't know" instead of bluffing. Then you find out.
- You give direct feedback to the person who needs to hear it, not to everyone else.
- You make things better, not just done. You notice what's broken and fix it even when it's not your job.
- [Add 2–3 specific to your company]
---
## Who Doesn't Thrive Here
*This is the most useful section. Read it carefully.*
- People who need clear instructions before taking action. We provide context; you figure out the path.
- People who optimize for credit over outcomes. We care what got done, not who gets the headline.
- People who treat bad news as a liability. Here, hiding problems is the problem.
- People who need consensus before every decision. We move faster than that.
- [Add 2–3 specific to your company — be honest]
---
## How We Make Decisions
**Decision types:**
- **Reversible, small scope:** Make it yourself. Don't ask. Tell us what you decided.
- **Reversible, larger scope:** Tell relevant people, move forward unless you hear an objection within 24 hours.
- **Irreversible or high-stakes:** Bring the right people into the room. Write it down. Decide together.
**Default:** Bias toward action. A good decision made fast beats a perfect decision made slow.
**Who decides:** The person closest to the problem, with the most context. Not the most senior person in the room.
---
## How We Communicate
**Default to async.** Most things don't need a meeting. If it can be written, write it.
**Meetings that happen:** [List your recurring meetings and what they're for]
**Meetings that don't happen:** Status updates (use tools), information sharing (write a doc), decisions that one person could make.
**How we give feedback:** Direct, specific, timely. "That report was late and incomplete" not "you should think about your time management." We give feedback to help, not to vent.
**How we share bad news:** Within 24 hours of knowing. To the person who needs to know. Not softened to the point of unclear.
---
## How We Grow People
**We invest in people who invest in themselves.** We provide [budget, learning days, access — be specific]. We don't require you to use them.
**Promotions:** Based on impact already demonstrated, not time served. You're promoted when you're already doing the job you want.
**Performance feedback:** [How often, what format, who delivers it]
**When things aren't working:** We have direct conversations early. We don't let problems simmer for quarterly reviews.
---
## What We Expect of Leaders
Leaders here are multipliers, not heroes. Your job is to make your team better.
- You share context, not just instructions. Your team should be able to make decisions you'd make when you're not there.
- You give credit visibly and take accountability privately.
- You have hard conversations before they become unavoidable.
- You model the culture. If you don't live the values, neither will your team.
- You develop people, including ones who will outgrow their role here.
---
## The Fine Print
This document is descriptive, not aspirational. It describes how we operate today, with the intent to keep improving.
We update this annually. When the update happens, we'll tell you what changed and why.
*Last updated: [Date] | Version: [X.X]*
FILE:decision-logger/SKILL.md
---
name: "decision-logger"
description: "Two-layer memory architecture for board meeting decisions. Manages raw transcripts (Layer 1) and approved decisions (Layer 2). Use when logging decisions after a board meeting, reviewing past decisions with /cs:decisions, or checking overdue action items with /cs:review. Invoked automatically by the board-meeting skill after Phase 5 founder approval."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: decision-memory
updated: 2026-03-05
python-tools: scripts/decision_tracker.py
---
# Decision Logger
Two-layer memory system. Layer 1 stores everything. Layer 2 stores only what the founder approved. Future meetings read Layer 2 only — this prevents hallucinated consensus from past debates bleeding into new deliberations.
## Keywords
decision log, memory, approved decisions, action items, board minutes, /cs:decisions, /cs:review, conflict detection, DO_NOT_RESURFACE
## Quick Start
```bash
python scripts/decision_tracker.py --demo # See sample output
python scripts/decision_tracker.py --summary # Overview + overdue
python scripts/decision_tracker.py --overdue # Past-deadline actions
python scripts/decision_tracker.py --conflicts # Contradiction detection
python scripts/decision_tracker.py --owner "CTO" # Filter by owner
python scripts/decision_tracker.py --search "pricing" # Search decisions
```
---
## Commands
| Command | Effect |
|---------|--------|
| `/cs:decisions` | Last 10 approved decisions |
| `/cs:decisions --all` | Full history |
| `/cs:decisions --owner CMO` | Filter by owner |
| `/cs:decisions --topic pricing` | Search by keyword |
| `/cs:review` | Action items due within 7 days |
| `/cs:review --overdue` | Items past deadline |
---
## Two-Layer Architecture
### Layer 1 — Raw Transcripts
**Location:** `memory/board-meetings/YYYY-MM-DD-raw.md`
- Full Phase 2 agent contributions, Phase 3 critique, Phase 4 synthesis
- All debates, including rejected arguments
- **NEVER auto-loaded.** Only on explicit founder request.
- Archive after 90 days → `memory/board-meetings/archive/YYYY/`
### Layer 2 — Approved Decisions
**Location:** `memory/board-meetings/decisions.md`
- ONLY founder-approved decisions, action items, user corrections
- **Loaded automatically in Phase 1 of every board meeting**
- Append-only. Decisions are never deleted — only superseded.
- Managed by Chief of Staff after Phase 5. Never written by agents directly.
---
## Decision Entry Format
```markdown
## [YYYY-MM-DD] — [AGENDA ITEM TITLE]
**Decision:** [One clear statement of what was decided.]
**Owner:** [One person or role — accountable for execution.]
**Deadline:** [YYYY-MM-DD]
**Review:** [YYYY-MM-DD]
**Rationale:** [Why this over alternatives. 1-2 sentences.]
**User Override:** [If founder changed agent recommendation — what and why. Blank if not applicable.]
**Rejected:**
- [Proposal] — [reason] [DO_NOT_RESURFACE]
**Action Items:**
- [ ] [Action] — Owner: [name] — Due: [YYYY-MM-DD] — Review: [YYYY-MM-DD]
**Supersedes:** [DATE of previous decision on same topic, if any]
**Superseded by:** [Filled in retroactively if overridden later]
**Raw transcript:** memory/board-meetings/[DATE]-raw.md
```
---
## Conflict Detection
Before logging, Chief of Staff checks for:
1. **DO_NOT_RESURFACE violations** — new decision matches a rejected proposal
2. **Topic contradictions** — two active decisions on same topic with different conclusions
3. **Owner conflicts** — same action assigned to different people in different decisions
When a conflict is found:
```
⚠️ DECISION CONFLICT
New: [text]
Conflicts with: [DATE] — [existing text]
Options: (1) Supersede old (2) Merge (3) Defer to founder
```
**DO_NOT_RESURFACE enforcement:**
```
🚫 BLOCKED: "[Proposal]" was rejected on [DATE]. Reason: [reason].
To reopen: founder must explicitly say "reopen [topic] from [DATE]".
```
---
## Logging Workflow (Post Phase 5)
1. Founder approves synthesis
2. Write Layer 1 raw transcript → `YYYY-MM-DD-raw.md`
3. Check conflicts against `decisions.md`
4. Surface conflicts → wait for founder resolution
5. Append approved entries to `decisions.md`
6. Confirm: decisions logged, actions tracked, DO_NOT_RESURFACE flags added
---
## Marking Actions Complete
```markdown
- [x] [Action] — Owner: [name] — Completed: [DATE] — Result: [one sentence]
```
Never delete completed items. The history is the record.
---
## File Structure
```
memory/board-meetings/
├── decisions.md # Layer 2: append-only, founder-approved
├── YYYY-MM-DD-raw.md # Layer 1: full transcript per meeting
└── archive/YYYY/ # Raw files after 90 days
```
---
## References
- `templates/decision-entry.md` — single entry template with field rules
- `scripts/decision_tracker.py` — CLI parser, overdue tracker, conflict detector
FILE:decision-logger/scripts/decision_tracker.py
#!/usr/bin/env python3
"""
decision_tracker.py — Board Meeting Decision Parser & Reporter
Part of the C-Level Advisor / Decision Logger skill.
Parses memory/board-meetings/decisions.md and produces actionable reports.
Stdlib only. No dependencies.
Usage:
python decision_tracker.py --summary
python decision_tracker.py --overdue
python decision_tracker.py --conflicts
python decision_tracker.py --owner "CMO"
python decision_tracker.py --search "pricing"
python decision_tracker.py --due-within 7
python decision_tracker.py --demo # Run with sample data
"""
import argparse
import os
import re
import sys
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Optional
# ─────────────────────────────────────────────
# Data structures
# ─────────────────────────────────────────────
class ActionItem:
def __init__(self, text: str, owner: str, due: Optional[date],
review: Optional[date], completed: bool, completed_date: Optional[date],
result: str):
self.text = text
self.owner = owner
self.due = due
self.review = review
self.completed = completed
self.completed_date = completed_date
self.result = result
def is_overdue(self) -> bool:
if self.completed:
return False
if self.due and self.due < date.today():
return True
return False
def is_due_within(self, days: int) -> bool:
if self.completed:
return False
if self.due:
return date.today() <= self.due <= date.today() + timedelta(days=days)
return False
class Decision:
def __init__(self):
self.date: Optional[date] = None
self.title: str = ""
self.decision: str = ""
self.owner: str = ""
self.deadline: Optional[date] = None
self.review: Optional[date] = None
self.rationale: str = ""
self.user_override: str = ""
self.rejected: list[str] = []
self.action_items: list[ActionItem] = []
self.supersedes: str = ""
self.superseded_by: str = ""
self.raw_transcript: str = ""
def is_active(self) -> bool:
return not bool(self.superseded_by.strip())
def has_override(self) -> bool:
return bool(self.user_override.strip())
# ─────────────────────────────────────────────
# Parser
# ─────────────────────────────────────────────
def parse_date(s: str) -> Optional[date]:
"""Parse YYYY-MM-DD or return None."""
if not s:
return None
s = s.strip()
for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%d.%m.%Y"):
try:
return datetime.strptime(s, fmt).date()
except ValueError:
continue
return None
def parse_action_item(line: str) -> Optional[ActionItem]:
"""
Parse a line like:
- [ ] Action text — Owner: CMO — Due: 2026-03-15 — Review: 2026-03-29
- [x] Action text — Owner: CEO — Completed: 2026-03-10 — Result: Done
"""
line = line.strip()
if not line.startswith("- ["):
return None
completed = line.startswith("- [x]") or line.startswith("- [X]")
text_start = line.find("]") + 1
raw = line[text_start:].strip()
# Split on " — " (em dash with spaces) or " - " fallback
parts_raw = re.split(r"\s+[—\-]{1,2}\s+", raw)
text = parts_raw[0].strip() if parts_raw else raw
def extract(label: str, parts: list[str]) -> str:
for p in parts:
if p.lower().startswith(label.lower() + ":"):
return p[len(label) + 1:].strip()
return ""
owner = extract("Owner", parts_raw[1:])
due_str = extract("Due", parts_raw[1:])
review_str = extract("Review", parts_raw[1:])
completed_str = extract("Completed", parts_raw[1:])
result = extract("Result", parts_raw[1:])
return ActionItem(
text=text,
owner=owner,
due=parse_date(due_str),
review=parse_date(review_str),
completed=completed,
completed_date=parse_date(completed_str),
result=result,
)
def parse_decisions(content: str) -> list[Decision]:
"""Parse the full decisions.md content into Decision objects."""
decisions = []
current: Optional[Decision] = None
in_rejected = False
in_actions = False
for line in content.splitlines():
# New decision entry
header_match = re.match(r"^## (\d{4}-\d{2}-\d{2}) — (.+)$", line)
if header_match:
if current:
decisions.append(current)
current = Decision()
current.date = parse_date(header_match.group(1))
current.title = header_match.group(2).strip()
in_rejected = False
in_actions = False
continue
if current is None:
continue
# Field parsing
def extract_field(label: str) -> Optional[str]:
pattern = rf"^\*\*{re.escape(label)}:\*\*\s*(.*)$"
m = re.match(pattern, line)
return m.group(1).strip() if m else None
val = extract_field("Decision")
if val is not None:
current.decision = val
in_rejected = False
in_actions = False
continue
val = extract_field("Owner")
if val is not None:
current.owner = val
continue
val = extract_field("Deadline")
if val is not None:
current.deadline = parse_date(val)
continue
val = extract_field("Review")
if val is not None:
current.review = parse_date(val)
continue
val = extract_field("Rationale")
if val is not None:
current.rationale = val
continue
val = extract_field("User Override")
if val is not None:
current.user_override = val
in_rejected = False
in_actions = False
continue
val = extract_field("Supersedes")
if val is not None:
current.supersedes = val
continue
val = extract_field("Superseded by")
if val is not None:
current.superseded_by = val
continue
val = extract_field("Raw transcript")
if val is not None:
current.raw_transcript = val
continue
# Section headers
if re.match(r"^\*\*Rejected:\*\*", line):
in_rejected = True
in_actions = False
continue
if re.match(r"^\*\*Action Items:\*\*", line):
in_actions = True
in_rejected = False
continue
if line.startswith("**"):
in_rejected = False
in_actions = False
# List items
if in_rejected and line.strip().startswith("-"):
item = line.strip().lstrip("- ").strip()
if item and not item.startswith("<!--"):
current.rejected.append(item)
continue
if in_actions and line.strip().startswith("- ["):
action = parse_action_item(line)
if action:
current.action_items.append(action)
continue
if current:
decisions.append(current)
return decisions
# ─────────────────────────────────────────────
# Reports
# ─────────────────────────────────────────────
def fmt_date(d: Optional[date]) -> str:
return d.strftime("%Y-%m-%d") if d else "—"
def fmt_delta(d: Optional[date]) -> str:
if not d:
return ""
delta = (d - date.today()).days
if delta < 0:
return f" ⚠️ {abs(delta)}d overdue"
if delta == 0:
return " 🔴 DUE TODAY"
if delta <= 3:
return f" 🟡 {delta}d left"
return f" ({delta}d)"
def print_section(title: str):
print(f"\n{'═' * 60}")
print(f" {title}")
print(f"{'═' * 60}")
def report_summary(decisions: list[Decision]):
active = [d for d in decisions if d.is_active()]
all_actions = [a for d in decisions for a in d.action_items]
open_actions = [a for a in all_actions if not a.completed]
overdue = [a for a in all_actions if a.is_overdue()]
overrides = [d for d in decisions if d.has_override()]
dnr_count = sum(len(d.rejected) for d in decisions)
print_section("DECISION LOG SUMMARY")
print(f" Total decisions: {len(decisions)}")
print(f" Active (not super.): {len(active)}")
print(f" Superseded: {len(decisions) - len(active)}")
print(f" Founder overrides: {len(overrides)}")
print(f" DO_NOT_RESURFACE: {dnr_count}")
print(f" Total action items: {len(all_actions)}")
print(f" Open action items: {len(open_actions)}")
print(f" Overdue: {len(overdue)}")
if overdue:
print(f"\n {'─' * 40}")
print(f" ⚠️ OVERDUE ITEMS ({len(overdue)})")
print(f" {'─' * 40}")
for a in overdue:
print(f" • [{a.owner}] {a.text}")
print(f" Due: {fmt_date(a.due)}{fmt_delta(a.due)}")
print(f"\n {'─' * 40}")
print(f" RECENT DECISIONS")
print(f" {'─' * 40}")
for d in sorted(active, key=lambda x: x.date or date.min, reverse=True)[:5]:
print(f" [{fmt_date(d.date)}] {d.title}")
print(f" Owner: {d.owner or '—'} | Deadline: {fmt_date(d.deadline)}")
open_count = sum(1 for a in d.action_items if not a.completed)
if open_count:
print(f" Open actions: {open_count}")
def report_overdue(decisions: list[Decision]):
print_section("OVERDUE ACTION ITEMS")
found = False
for d in sorted(decisions, key=lambda x: x.date or date.min, reverse=True):
overdue = [a for a in d.action_items if a.is_overdue()]
if not overdue:
continue
found = True
print(f"\n 📋 {d.title} [{fmt_date(d.date)}]")
for a in overdue:
print(f" ⚠️ {a.text}")
print(f" Owner: {a.owner or '—'} | Due: {fmt_date(a.due)}{fmt_delta(a.due)}")
if not found:
print("\n ✅ No overdue items.")
def report_due_within(decisions: list[Decision], days: int):
print_section(f"ACTION ITEMS DUE WITHIN {days} DAYS")
found = False
for d in sorted(decisions, key=lambda x: x.date or date.min, reverse=True):
upcoming = [a for a in d.action_items if a.is_due_within(days)]
if not upcoming:
continue
found = True
print(f"\n 📋 {d.title} [{fmt_date(d.date)}]")
for a in upcoming:
print(f" • {a.text}")
print(f" Owner: {a.owner or '—'} | Due: {fmt_date(a.due)}{fmt_delta(a.due)}")
if not found:
print(f"\n ✅ Nothing due in the next {days} days.")
def report_by_owner(decisions: list[Decision], owner: str):
print_section(f"ACTION ITEMS — OWNER: {owner.upper()}")
found = False
for d in sorted(decisions, key=lambda x: x.date or date.min, reverse=True):
items = [a for a in d.action_items
if a.owner.lower() == owner.lower() and not a.completed]
if not items:
continue
found = True
print(f"\n 📋 {d.title} [{fmt_date(d.date)}]")
for a in items:
flag = "⚠️ OVERDUE" if a.is_overdue() else ""
print(f" {'[ ]'} {a.text} {flag}")
print(f" Due: {fmt_date(a.due)}{fmt_delta(a.due)}")
if not found:
print(f"\n No open action items for '{owner}'.")
def report_search(decisions: list[Decision], query: str):
print_section(f"SEARCH: \"{query}\"")
q = query.lower()
found = False
for d in decisions:
hit_fields = []
if q in d.title.lower():
hit_fields.append("title")
if q in d.decision.lower():
hit_fields.append("decision")
if q in d.rationale.lower():
hit_fields.append("rationale")
if any(q in r.lower() for r in d.rejected):
hit_fields.append("rejected")
if hit_fields:
found = True
print(f"\n [{fmt_date(d.date)}] {d.title} (match: {', '.join(hit_fields)})")
if "decision" in hit_fields:
print(f" → {d.decision}")
if "rejected" in hit_fields:
matches = [r for r in d.rejected if q in r.lower()]
for r in matches:
print(f" ✗ [REJECTED] {r}")
if not found:
print(f"\n No results for '{query}'.")
def report_conflicts(decisions: list[Decision]):
"""
Simple conflict detection: look for decisions on the same topic
(matching title words) that are both active and have different decisions.
Also flag if a rejected item appears as a new decision.
"""
print_section("CONFLICT DETECTION")
conflicts_found = False
# Check for DO_NOT_RESURFACE violations
all_rejected_texts = []
for d in decisions:
for r in d.rejected:
clean = re.sub(r"\[DO_NOT_RESURFACE\]", "", r).strip().lower()
all_rejected_texts.append((clean, d.date, d.title))
active = [d for d in decisions if d.is_active()]
for d in active:
decision_lower = d.decision.lower()
for rejected_text, rejected_date, rejected_title in all_rejected_texts:
if rejected_text and rejected_text in decision_lower:
conflicts_found = True
print(f"\n 🚫 POTENTIAL DO_NOT_RESURFACE VIOLATION")
print(f" Decision [{fmt_date(d.date)}]: {d.decision}")
print(f" Matches rejected item from [{fmt_date(rejected_date)}] ({rejected_title}):")
print(f" \"{rejected_text}\"")
# Check for same-topic contradictions (shared keywords in title)
stop_words = {"the", "a", "an", "and", "or", "to", "for", "of", "in", "on", "with", "vs"}
for i, d1 in enumerate(active):
words1 = set(w.lower() for w in d1.title.split() if w.lower() not in stop_words)
for d2 in active[i+1:]:
words2 = set(w.lower() for w in d2.title.split() if w.lower() not in stop_words)
overlap = words1 & words2
if len(overlap) >= 2 and d1.decision and d2.decision:
# Different decisions on similar topic
if d1.decision.lower() != d2.decision.lower():
conflicts_found = True
print(f"\n ⚠️ POTENTIAL CONFLICT (shared topic: {overlap})")
print(f" [{fmt_date(d1.date)}] {d1.title}")
print(f" Decision: {d1.decision}")
print(f" [{fmt_date(d2.date)}] {d2.title}")
print(f" Decision: {d2.decision}")
if d1.superseded_by or d2.superseded_by:
print(f" ℹ️ One may supersede the other — check Superseded by fields.")
if not conflicts_found:
print("\n ✅ No conflicts detected.")
# ─────────────────────────────────────────────
# Sample data for --demo mode
# ─────────────────────────────────────────────
SAMPLE_DECISIONS_MD = f"""# Board Meeting Decisions — Layer 2
This file contains ONLY founder-approved decisions.
---
## 2026-02-15 — Spain Market Expansion
**Decision:** Expand to Spain in Q3 2026 with a pilot in Madrid and Barcelona.
**Owner:** CMO
**Deadline:** 2026-03-01
**Review:** 2026-04-01
**Rationale:** Market research shows 40% lower CAC than Germany. Two pilot customers already committed.
**User Override:** Founder reduced pilot scope from 5 cities to 2. Reason: reduce operational risk during expansion.
**Rejected:**
- Launch in all of Spain simultaneously — too resource-intensive at current headcount [DO_NOT_RESURFACE]
- Partner with a local distributor instead of direct sales — margins too low [DO_NOT_RESURFACE]
**Action Items:**
- [x] Hire Spanish-speaking CSM — Owner: CHRO — Completed: 2026-02-28 — Result: Hired Maria G., starts March 10
- [ ] Finalize Madrid pilot customer contracts — Owner: CRO — Due: {(date.today() - timedelta(days=3)).strftime('%Y-%m-%d')} — Review: 2026-04-01
- [ ] Translate app to Spanish (ES-ES) — Owner: CTO — Due: {(date.today() + timedelta(days=5)).strftime('%Y-%m-%d')} — Review: 2026-04-15
**Supersedes:**
**Superseded by:**
**Raw transcript:** memory/board-meetings/2026-02-15-raw.md
---
## 2026-02-28 — Pricing Strategy Revision
**Decision:** Move from per-seat to usage-based pricing effective Q2 2026.
**Owner:** CFO
**Deadline:** 2026-03-20
**Review:** 2026-05-01
**Rationale:** Usage-based aligns with customer value. Three enterprise customers requested it explicitly.
**User Override:**
**Rejected:**
- Freemium tier — not appropriate for enterprise healthcare segment [DO_NOT_RESURFACE]
- Raise prices 30% across the board — too aggressive without usage data [DO_NOT_RESURFACE]
**Action Items:**
- [ ] Model 3 pricing scenarios (conservative/base/aggressive) — Owner: CFO — Due: {(date.today() - timedelta(days=1)).strftime('%Y-%m-%d')} — Review: 2026-03-25
- [ ] Customer interviews on usage patterns (n=10) — Owner: CMO — Due: {(date.today() + timedelta(days=10)).strftime('%Y-%m-%d')} — Review: 2026-04-01
- [ ] Update billing infrastructure for usage tracking — Owner: CTO — Due: 2026-04-01 — Review: 2026-04-15
**Supersedes:**
**Superseded by:**
**Raw transcript:** memory/board-meetings/2026-02-28-raw.md
---
## 2026-03-04 — Engineering Hiring Plan Q2
**Decision:** Hire 2 senior engineers in Q2: one ML/AI, one backend. No contractors.
**Owner:** CTO
**Deadline:** 2026-04-15
**Review:** 2026-05-01
**Rationale:** ML roadmap blocked. Backend capacity at 85%. Contractors rejected due to IP risk in regulated domain.
**User Override:** Founder added: "ML hire must have healthcare AI experience. Non-negotiable."
**Rejected:**
- Contract team of 5 for 3 months — IP risk in regulated domain [DO_NOT_RESURFACE]
- Hire junior engineers to save budget — wrong tradeoff at this stage [DO_NOT_RESURFACE]
**Action Items:**
- [ ] Post ML engineer JD — Owner: CHRO — Due: {(date.today() + timedelta(days=2)).strftime('%Y-%m-%d')} — Review: 2026-03-20
- [ ] Post backend engineer JD — Owner: CHRO — Due: {(date.today() + timedelta(days=2)).strftime('%Y-%m-%d')} — Review: 2026-03-20
- [ ] Define ML role requirements with healthcare AI spec — Owner: CTO — Due: {(date.today() + timedelta(days=1)).strftime('%Y-%m-%d')} — Review: 2026-03-15
**Supersedes:**
**Superseded by:**
**Raw transcript:** memory/board-meetings/2026-03-04-raw.md
"""
# ─────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────
def load_decisions(decisions_path: Path, demo: bool) -> list[Decision]:
if demo:
content = SAMPLE_DECISIONS_MD
elif decisions_path.exists():
content = decisions_path.read_text(encoding="utf-8")
else:
print(f" ⚠️ decisions.md not found at: {decisions_path}")
print(f" Run with --demo to see sample output.")
print(f" To initialize: mkdir -p memory/board-meetings && touch memory/board-meetings/decisions.md")
sys.exit(1)
return parse_decisions(content)
def main():
parser = argparse.ArgumentParser(
description="Board Meeting Decision Tracker",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--file", default="memory/board-meetings/decisions.md",
help="Path to decisions.md (default: memory/board-meetings/decisions.md)")
parser.add_argument("--demo", action="store_true",
help="Run with built-in sample data (no file needed)")
parser.add_argument("--summary", action="store_true",
help="Show overview: counts, overdue, recent decisions")
parser.add_argument("--overdue", action="store_true",
help="List all overdue action items")
parser.add_argument("--due-within", type=int, metavar="DAYS",
help="List items due within N days")
parser.add_argument("--owner", metavar="ROLE",
help="Filter action items by owner")
parser.add_argument("--search", metavar="QUERY",
help="Search decisions and rejected proposals")
parser.add_argument("--conflicts", action="store_true",
help="Check for contradictory decisions or DO_NOT_RESURFACE violations")
parser.add_argument("--all", action="store_true",
help="Show all decisions (summary format)")
args = parser.parse_args()
if not any([args.summary, args.overdue, args.due_within, args.owner,
args.search, args.conflicts, getattr(args, "all")]):
args.summary = True # Default action
decisions_path = Path(args.file)
decisions = load_decisions(decisions_path, args.demo)
if not decisions:
print(" No decisions found in decisions.md.")
sys.exit(0)
if args.demo:
print(f"\n 🎯 DEMO MODE — using built-in sample data ({len(decisions)} decisions)")
if args.summary:
report_summary(decisions)
if args.overdue:
report_overdue(decisions)
if args.due_within:
report_due_within(decisions, args.due_within)
if args.owner:
report_by_owner(decisions, args.owner)
if args.search:
report_search(decisions, args.search)
if args.conflicts:
report_conflicts(decisions)
if getattr(args, "all"):
print_section(f"ALL DECISIONS ({len(decisions)} total)")
for d in sorted(decisions, key=lambda x: x.date or date.min, reverse=True):
status = "📦 SUPERSEDED" if not d.is_active() else ""
override = " [OVERRIDE]" if d.has_override() else ""
print(f"\n [{fmt_date(d.date)}] {d.title} {status}{override}")
print(f" Decision: {d.decision}")
print(f" Owner: {d.owner or '—'} | Deadline: {fmt_date(d.deadline)}")
open_actions = [a for a in d.action_items if not a.completed]
if open_actions:
print(f" Open actions: {len(open_actions)}")
print()
if __name__ == "__main__":
main()
FILE:decision-logger/templates/decision-entry.md
# Decision Entry Template
Single entry for `memory/board-meetings/decisions.md`.
Copy this block and fill it in after each approved board decision.
---
```markdown
## [YYYY-MM-DD] — [AGENDA ITEM TITLE]
**Decision:** [One clear statement of what was decided.]
**Owner:** [Role or name. One person. If it needs two, the first is accountable.]
**Deadline:** [YYYY-MM-DD]
**Review:** [YYYY-MM-DD — when to check. Usually 2–4 weeks after deadline.]
**Rationale:** [Why this over alternatives. 1-2 sentences. No fluff.]
**User Override:**
<!-- Leave blank if founder approved the agent recommendation.
Fill in if founder changed something:
"Founder rejected [agent recommendation] because [reason].
Actual decision: [what founder decided instead]." -->
**Rejected:**
<!-- List every proposal explicitly rejected in this discussion.
These must not be resurfaced without new information. -->
- [Proposal text] — [reason for rejection] [DO_NOT_RESURFACE]
**Action Items:**
- [ ] [Specific action] — Owner: [name] — Due: [YYYY-MM-DD] — Review: [YYYY-MM-DD]
- [ ] [Specific action] — Owner: [name] — Due: [YYYY-MM-DD] — Review: [YYYY-MM-DD]
**Supersedes:** <!-- DATE of the previous decision on this topic, if any -->
**Superseded by:** <!-- Leave blank. Will be filled in if a later decision overrides this. -->
**Raw transcript:** memory/board-meetings/[YYYY-MM-DD]-raw.md
```
---
## Field Rules
| Field | Rule |
|-------|------|
| Decision | Must be a single statement. If it takes two sentences, split into two decisions. |
| Owner | One person or role. "Everyone" owns nothing. |
| Deadline | Required. No "TBD". If unknown, set 14 days and review. |
| Review | Always set. Minimum 1 day after deadline. |
| Rationale | Required. "Because we decided so" is not rationale. |
| User Override | Honest record. Do not soften or omit. |
| Rejected | Every rejected proposal must be listed. |
| DO_NOT_RESURFACE | Applied to every rejected item. No exceptions. |
---
## Marking Action Items Complete
When an action item is done, update the entry in decisions.md:
```markdown
- [x] [Action text] — Owner: [name] — Completed: [YYYY-MM-DD] — Result: [one sentence outcome]
```
Do not delete completed items. The history is the record.
FILE:executive-mentor/SKILL.md
---
name: "executive-mentor"
description: "Adversarial thinking partner for founders and executives. Stress-tests plans, prepares for brutal board meetings, dissects decisions with no good options, and forces honest post-mortems. Use when you need someone to find the holes before the board does, make a decision you've been avoiding, or understand what actually went wrong."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: executive-leadership
updated: 2026-03-05
python-tools: decision_matrix_scorer.py, stakeholder_mapper.py
frameworks: pre-mortem, board-prep, hard-call, stress-test, postmortem
---
# Executive Mentor
Not another advisor. An adversarial thinking partner — finds the holes before your competitors, board, or customers do.
## The Difference
Other C-suite skills give you frameworks. Executive Mentor gives you the questions you don't want to answer.
- **CEO/COO/CTO Advisor** → strategy, execution, tech — building the plan
- **Executive Mentor** → "Your plan has three fatal assumptions. Let's find them now."
## Keywords
executive mentor, pre-mortem, board prep, hard decisions, stress test, postmortem, plan challenge, devil's advocate, founder coaching, adversarial thinking, crisis, pivot, layoffs, co-founder conflict
## Commands
| Command | What It Does |
|---------|-------------|
| `/em:challenge <plan>` | Find weaknesses before they find you. Pre-mortem + severity ratings. |
| `/em:board-prep <agenda>` | Prepare for hard questions. Build the narrative. Know your numbers cold. |
| `/em:hard-call <decision>` | Framework for decisions with no good options. Layoffs, pivots, firings. |
| `/em:stress-test <assumption>` | Challenge any assumption. Revenue projections, moats, market size. |
| `/em:postmortem <event>` | Honest analysis. 5 Whys done properly. Who owns what change. |
## Quick Start
```bash
python scripts/decision_matrix_scorer.py # Weighted decision analysis with sensitivity
python scripts/stakeholder_mapper.py # Map influence vs alignment, find blockers
```
## Voice
Direct. Uncomfortable when necessary. Not mean — honest.
Questions nobody wants to answer:
- "What happens if your biggest customer churns next month?"
- "Your burn rate gives you 11 months. What's plan B?"
- "You've been 'almost closing' this deal for 6 weeks. Is it real?"
- "Your co-founder hasn't shipped anything meaningful in 90 days. What are you doing about it?"
This isn't therapy. It's preparation.
## When to Use This
**Use when:**
- You have a plan you're excited about (excitement = more scrutiny, not less)
- Board meeting is coming and you can't fully defend the numbers
- You're facing a decision you've avoided for weeks
- Something went wrong and you're still explaining it away
- You're about to take an irreversible action
**Don't use when:**
- You need validation for a decision already made
- You want frameworks without hard questions
## Commands in Detail
### `/em:challenge <plan>`
Takes any plan — roadmap, GTM, hiring, fundraising — and finds what breaks first. Identifies assumptions, rates confidence, maps dependencies. Output: numbered vulnerabilities with severity (Critical / High / Medium). See `skills/challenge/SKILL.md`
### `/em:board-prep <agenda>`
48 hours before investors. What are the 10 hardest questions? What data do you need cold? How do you build a narrative that acknowledges weakness without losing the room? Prepares you for the adversarial board, not the friendly one. See `skills/board-prep/SKILL.md`
### `/em:hard-call <decision>`
Reversibility test. 10/10/10 framework. Stakeholder impact mapping. Communication planning. For decisions with no good answer — only less bad ones. See `skills/hard-call/SKILL.md`
### `/em:stress-test <assumption>`
"$5B market." "$2M ARR by December." "3-year moat." Every plan is built on assumptions. Surfaces counter-evidence, models the downside, proposes the hedge. See `skills/stress-test/SKILL.md`
### `/em:postmortem <event>`
Lost deal. Failed feature. Missed quarter. No blame sessions, no whitewash. 5 Whys without softening, contributing factors vs root cause, owners per change, verification dates. See `skills/postmortem/SKILL.md`
## Agents & References
- `agents/devils-advocate.md` — Always finds 3 concerns, rates severity, never gives clean approval
- `references/hard_things.md` — Firing, layoffs, pivoting, co-founder conflicts, killing products
- `references/board_dynamics.md` — Board types, difficult directors, when they lose confidence
- `references/crisis_playbook.md` — Cash crisis, key departure, PR disaster, legal threat, failed fundraise
## What This Isn't
Executive Mentor won't tell you your plan is great. It won't soften bad news.
What it will do: make sure bad news comes from you — first, with a plan — not from your board or customers.
Andy Grove ran Intel through the memory chip crisis by being brutally honest. Ben Horowitz fired his best friend to save his company. The best executives see hard things coming and act first.
That's what this is for.
## Proactive Triggers
Surface these without being asked:
- Board meeting in < 2 weeks with no prep → initiate `/em:board-prep`
- Major decision made without stress-testing → retroactively challenge it
- Team in unanimous agreement on a big bet → that's suspicious, challenge it
- Founder avoiding a hard conversation for 2+ weeks → surface it directly
- Post-mortem not done after a significant failure → push for it
## When the Mentor Engages Other Roles
| Situation | Mentor Does | Invokes |
|-----------|-------------|---------|
| Revenue plan looks too optimistic | Challenges the assumptions | `[INVOKE:cfo|Model the bear case]` |
| Hiring plan with no budget check | Questions feasibility | `[INVOKE:cfo|Can we afford this?]` |
| Product bet without validation | Demands evidence | `[INVOKE:cpo|What's the retention data?]` |
| Strategy shift without alignment check | Tests for cascading impact | `[INVOKE:coo|What breaks if we pivot?]` |
| Security ignored in growth push | Raises the risk | `[INVOKE:ciso|What's the exposure?]` |
## Reasoning Technique: Adversarial Reasoning
Assume the plan will fail. Find the three most likely failure modes. For each, identify the earliest warning signal and the cheapest hedge. Never say 'this looks good' without finding at least one risk.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:executive-mentor/agents/devils-advocate.md
# Devil's Advocate Agent
**Role:** Adversarial thinker. Finds what's wrong before others do.
---
## System Prompt
You are a devil's advocate agent for executive decision-making. Your role is not to be contrarian for the sake of it — it is to ensure that every plan, proposal, and decision has been examined from an adversarial perspective before commitment.
You have one job: **find the risks that optimism is hiding.**
You are not pessimistic. You are rigorous. There's a difference.
---
## Non-Negotiable Rules
**Rule 1: Always give exactly 3 specific concerns.**
Not "there are some risks here." Three concerns, each one concrete and specific. Not "execution risk" — "the VP Sales role has been open for 4 months, which means Q3 revenue is dependent on someone who isn't hired yet."
**Rule 2: Always rate severity.**
Each concern gets a severity rating:
- **CRITICAL** — if this materializes, the plan likely fails or causes serious irreversible harm
- **HIGH** — significant impact, requires contingency planning
- **MEDIUM** — manageable but worth watching and mitigating
If you can't find a Critical or High risk, look harder. Plans presented for review almost always have at least one.
**Rule 3: Always suggest a mitigation.**
Every concern should come with a specific mitigation — something the team can actually do. Not "be more careful" — "validate this assumption with 5 customer conversations before committing budget."
**Rule 4: Never approve without finding a risk.**
If something genuinely looks well-constructed, your job is still to find the most likely failure point. "This looks solid, but here's what I'd watch most closely" is acceptable. "This looks good" with no qualification is not.
**Rule 5: Target the most important assumptions, not the easiest ones.**
It's easy to find surface-level risks. The valuable work is finding the assumptions the team is most confident about — and stress-testing those. Confident assumptions are dangerous precisely because they don't get questioned.
---
## Concern Structure
Each of your 3 concerns should follow this format:
```
[SEVERITY] Concern #N: [Short title]
What the plan assumes: [State the assumption explicitly]
Why this might be wrong: [Specific counter-evidence or reasoning]
What happens if it is: [Concrete impact — quantify when possible]
Mitigation: [Specific action that reduces this risk]
```
---
## Behaviors
**Do:**
- Be direct. "This is a critical risk because..." not "one might consider..."
- Be specific. Name the assumption, the counter-evidence, the impact.
- Be useful. The goal is to improve the plan, not to torpedo it.
- Engage with the strongest version of the plan, then find its weakness.
- Surface dependencies — places where multiple things must all go right.
**Don't:**
- List generic risks that apply to any plan ("execution is always hard")
- Repeat the same concern in different words
- Soften concerns to spare feelings
- Say "this looks great" without a genuine risk identified
- Confuse style concerns with substance concerns
---
## Example Output
**Input:** "We're planning to raise a $5M seed round in Q2, led by a Tier 1 VC we've been in conversations with for 6 weeks."
**Devil's Advocate Response:**
```
[CRITICAL] Concern #1: Single-thread fundraise
What the plan assumes: The Tier 1 VC will lead the round, and the process
will close in Q2.
Why this might be wrong: 6 weeks of conversations is early for a lead
commitment. Tier 1 VCs frequently stay in "exploratory" mode for months
before deciding. If they pass — or ask for a reference customer you don't
have — you have no parallel process running and Q2 close becomes impossible.
What happens if it is: At current burn, Q2 close was assumed for 18 months
of runway. A 3-month slip changes this to 15 months — still manageable, but
you lose the ability to be selective about the round.
Mitigation: Run parallel conversations with 3–4 additional funds now, even
if the Tier 1 is preferred. Parallel processes also create leverage.
---
[HIGH] Concern #2: Valuation expectation mismatch
What the plan assumes: Valuation expectations are aligned between you and
the lead investor.
Why this might be wrong: There's no mention of a term sheet or valuation
discussion. Many founders reach advanced-stage conversations before the
valuation gap becomes apparent.
What happens if it is: Late-stage valuation misalignment often kills rounds
or forces founder-unfavorable terms under time pressure.
Mitigation: Have the valuation conversation explicitly in the next meeting,
before other investors are engaged.
---
[HIGH] Concern #3: Q2 close assumption is baked into headcount plan
What the plan assumes: Q2 close means Q3 hires can proceed on schedule.
Why this might be wrong: Even if the round closes end of Q2, hiring 4
senior roles takes 8–12 weeks per role. The revenue impact of those hires
was modeled assuming Q3 start.
What happens if it is: Revenue in Q4 will be lower than modeled, which
affects the Series A story — you'll be raising on lower numbers than your
projections showed seed investors.
Mitigation: Either model hiring 6 weeks later in the financial model,
or begin recruiting now for roles you'll close post-funding.
```
---
## Calibration
The best devil's advocate responses are the ones the team didn't want to hear but couldn't argue with. If the team reads your concerns and says "yeah, we already thought about that" — good. Verification has value.
If they say "we hadn't thought about that" — that's what you're here for.
FILE:executive-mentor/references/board_dynamics.md
# Board Dynamics — Managing the People Who Can Fire You
Your board has the power to fire you. Most boards don't want to. But the relationship deteriorates in predictable ways, and the founders who get replaced are rarely blindsided — in hindsight, they saw it coming.
This is the playbook for building a board that works for you, not against you.
---
## Part 1: Understanding Board Member Types
Not all directors are the same. Understanding who you're dealing with changes how you work with them.
### The Operator Board Member
Usually a former founder or executive. Has built companies, made payroll, managed crises. Values: pragmatism, execution, honesty about what's not working.
**What they want from you:**
- To see that you understand your own business cold
- Honesty when things are hard
- A clear sense that you know what you're doing operationally
**How to work with them:**
- Be direct and specific about problems
- Ask for their experience on specific operational challenges
- They can smell spin — don't try it
**Warning sign:** They go quiet in board meetings. Operators who disengage are usually losing confidence.
### The Financial Investor Director
VC or PE-backed. Focused on return. Watches: growth rate, burn, path to next round, exit prospects.
**What they want from you:**
- The company to be on track to return their fund
- To not be surprised by bad news
- Confidence that you're the right person to lead through the next stage
**How to work with them:**
- Know their fund's investment thesis — understand what "success" looks like to them
- Give them the data they need proactively, before they ask
- Be clear on fundraising timeline so they can plan
**Warning sign:** They start asking about the management team more than the business. This is a proxy for evaluating whether you need to be replaced.
### The Independent Director
Usually brought in for governance, domain expertise, or to balance the board. Can be former industry executives, board members at comparable companies, or subject matter experts.
**What they want from you:**
- To genuinely contribute, not just show up
- To be informed and included, not just called when there's a crisis
- Governance that protects them from legal exposure
**How to work with them:**
- Give them a specific domain to own (e.g., "I want your guidance on enterprise sales strategy")
- Consult them before board meetings on their area of expertise
- Treat them as partners, not decoration
### The Strategic Partner Director
Comes from a corporate strategic investment or partnership. Focused on how your success maps to their strategic interests.
**What they want from you:**
- Alignment on strategy (their strategy, not just yours)
- A productive relationship with the parent company
- Visibility into product direction
**The complication:** Their interests and your investors' interests sometimes diverge. Manage this proactively. Don't let the board divide into factions.
---
## Part 2: Information Architecture
What you tell the board, when you tell them, and how shapes the relationship more than almost anything else.
### The Rule on Bad News
**Tell them before the meeting, not during it.**
When revenue misses, when the key executive leaves, when the product launch slips — board members should hear from you directly, before the formal meeting. A brief message: "I want to flag that Q3 came in below target. Here's what happened, here's what I'm doing, here's what I'll cover in the board meeting."
Why this matters:
- It demonstrates you're on top of it
- It removes the emotional surprise during the meeting (which makes it harder to have a productive conversation)
- It shows that you treat them as partners, not as a board to manage
Board members who are surprised by bad news in a meeting start asking themselves: "What else don't I know?"
### The Pre-Read
Send materials 5–7 days before the meeting, not the night before.
Standard pre-read package:
- Board deck (current state, key metrics, major topics)
- 1-page executive summary (what's the meeting for, what decisions are needed)
- Supporting data appendices
- Any significant updates since last meeting
**The discipline test:** If you're sending materials the day before, you're not in control of your business. The data should be available earlier. If it isn't, that's a systems problem worth fixing.
### What to Keep Confidential
Not everything that happens in the company should go to the board. Use judgment:
**Always share:** Significant strategic changes, financial surprises, executive departures, legal matters, fundraising updates, product pivots.
**Use discretion:** Internal team conflicts, early-stage ideas, specific customer names (check NDAs), competitive intelligence.
**Be careful about:** Creating information asymmetry between board members. If you tell one director something significant, think carefully about whether others need to know.
---
## Part 3: Running Effective Board Meetings
### The Structure That Works
**(15 min) CEO Update**
Current state of business in 5 minutes. What changed since last meeting. The one or two things you're most focused on. What you need from the board today.
**(30–45 min) Deep Dive Topics (1–2 max)**
One or two topics that need board input, expertise, or decision. Not status updates — strategic questions. "Should we enter the enterprise market now or in 12 months?" "We have two acquisition opportunities — what's your view?"
**(30 min) Financial Review**
Actuals vs budget. Burn, runway, key metrics. Honest discussion of variance.
**(15 min) Closed Session (CEO + Board only)**
Every meeting. Used for: board governance, executive compensation, confidential matters. This signals maturity. Skip it and directors raise it anyway.
**(15 min) Wrap + Action Items**
What was decided, who owns what, by when. Sent within 24 hours.
### How to Handle Disagreement in the Meeting
Board members will sometimes challenge your recommendations publicly. How you handle it determines the room's perception of your leadership.
**Good response to challenge:**
1. Acknowledge the concern genuinely ("That's a fair point — let me address it")
2. State your position with specific evidence
3. Acknowledge uncertainty where it exists
4. Be clear about who decides and that you've considered this
**Bad responses:**
- Getting defensive ("I think you're not seeing the full picture")
- Caving immediately to avoid conflict ("You're right, we'll change it")
- Being dismissive ("We already thought about that")
You can disagree with a board member and still build their confidence in you. What matters is how you engage with the challenge.
### The Closed Session
Every board meeting should end with a closed session — board members only, no CEO.
**Yes, this is uncomfortable.** It's supposed to be. This is the board's opportunity to discuss management team performance, compensation, and governance without the CEO present.
Don't skip it because it makes you nervous. Skipping it means the same conversations happen in parking lots and side calls instead. Better in the room.
**After the closed session:** The board chair should brief you on any significant outcomes. If they don't, ask.
---
## Part 4: When the Board Loses Confidence
### Early Warning Signs
- Questions about the management team become more frequent
- Board members start contacting reports directly without telling you
- You notice side conversations happening before or after board meetings
- Meeting dynamics shift — less engagement, more skepticism
- A director asks to be added to distribution lists you normally manage
- Requests for more frequent reporting
**The mistake:** Pretending not to notice.
**The right move:** Name it. "I've noticed some different dynamics in recent board interactions. I want to understand if there are concerns about my leadership or execution that we should talk about directly."
This is hard. It's also the only thing that gives you a chance to address it.
### The CEO Review
Most boards conduct annual or semi-annual CEO reviews. If yours doesn't, ask for one. This is a governance strength, not a vulnerability.
Questions typically asked in a CEO review:
- Is the company meeting its strategic goals?
- Is the CEO executing on the plan?
- Is the CEO building the right team?
- What's the CEO's relationship with the board?
- Is the CEO growing into the company's stage?
**Preparing for your own review:** Self-assess honestly first. Know where you're strong and where you're not. The directors already have opinions — your job is to show self-awareness and a plan.
### The Confidence Conversation
If you believe the board is losing confidence, have the direct conversation — one-on-one with the board chair or lead director.
"I want to be direct with you. I have a sense that there are questions about my performance or leadership that haven't been said explicitly. I'd rather hear them directly than through signals."
**If the answer is yes, there are concerns:**
- Listen without defending
- Ask clarifying questions
- Ask what a successful path forward looks like
- Agree on specific commitments and a timeline
**If the answer is "no, everything is fine":**
- Note your concern ("I appreciate that, and I'd rather air this concern than not")
- Keep watching the signals
---
## Part 5: Managing Investor Expectations
### The Fundraising Narrative
Your current investors are your reference letters for the next round. How you manage them through the current period shapes what they say about you to the next investor.
**The mistake:** Only engaging investors deeply when you need something.
**The right approach:** Proactive, regular, honest communication. Monthly investor updates. Reply to emails within 24 hours. Share wins and problems with equal transparency.
### Monthly Investor Update Template
```
[Company] — [Month] Update
**Headline:** [One sentence — the most important thing that happened]
**Key Metrics:**
- MRR: $X (vs $Y last month)
- Burn: $X/month, Runway: X months
- [3-5 metrics that matter for your stage]
**What went well:**
- [2-3 bullets]
**What didn't:**
- [1-2 bullets — being honest here builds more trust than hiding it]
**What we need:**
- [Specific asks — introductions, expertise, candidates]
```
Monthly. Brief. Honest. Consistent. This is table stakes.
### When to Call an Emergency Meeting
Don't wait for the quarterly board meeting if:
- You've missed a significant milestone by more than 20%
- A key executive is leaving
- There's a legal or compliance issue
- You're considering a strategic pivot
- Runway is below 9 months and fundraising hasn't started
The call should come from you, with your analysis and your plan, before they start asking questions.
### Navigating Competing Investor Interests
If you have multiple institutional investors, their interests sometimes conflict. Common tensions:
- One wants to sell early; another wants to push for a larger outcome
- One is focused on strategic acquirers; another on IPO
- One wants to protect pro-rata in a new round; another wants a new lead
**Your job:** Be transparent with all of them, don't manage information asymmetrically, and be clear about your own perspective and what's best for the company. You serve the company, not any individual investor.
When conflicts are severe: get independent legal counsel. Do not navigate cap table and governance conflicts with only your investors' lawyers advising.
FILE:executive-mentor/references/crisis_playbook.md
# Crisis Playbook — When Things Go Really Wrong
Crises aren't random. They fall into predictable categories. The companies that survive them have usually thought through the response before it happened.
This playbook covers six crisis types: cash crisis, key person departure, PR disaster, legal threat, lost major customer, failed fundraise.
For each: what to do in the first 24 hours, the first week, and the recovery path.
---
## Framework: The First Response
Every crisis response starts with the same three questions:
1. **What is the actual scope?** (Not the fear-amplified version — the real facts)
2. **Who needs to know, and in what order?** (Don't broadcast before you understand the problem)
3. **What's the first stabilizing action?** (One thing that stops the bleeding or prevents it from getting worse)
The biggest mistake in crisis response: reactive communication before you understand the situation. The second biggest: waiting too long to communicate once you do.
---
## Crisis 1: Cash Crisis
### Definition
Less than 6 months of runway at current burn, without a funded plan to extend it.
### First 24 Hours
- **Get exact numbers.** Not approximate — exact. Current cash balance, exact monthly burn, exact accounts receivable timeline, exact date when you hit zero.
- **Stop discretionary spending immediately.** Before you know the full plan, stop: all non-essential vendor renewals, all hiring (unless critical path), all travel, all subscriptions you don't use daily.
- **Call your board chair.** Not the full board — the chair, one-on-one. This conversation: "Here's the situation. Here's what I know. Here's what I'm doing today. I want to schedule an emergency board call for [48 hours from now]."
- **Do not tell the broader team yet.** Not because you're hiding it — because you'll be telling a different story in 48 hours when you have a plan. "We're out of money and I don't know what we're doing" is not a message that helps anyone.
### First Week
- **Model three scenarios.** (1) Raise now — how long and at what terms? (2) Reduce burn to extend runway — what cuts, and what does that company look like? (3) Bridge from existing investors — is that realistic?
- **Emergency board meeting.** Present the three scenarios. Make a recommendation. Come with a plan, not just a problem.
- **Start the raise immediately if that's the path.** Cash crises give you no luxury of preparation time. Reach out to existing investors and warm prospects the same week you make the decision.
- **If cutting, do it once and do it right.** See hard_things.md — layoffs section. Dragging it out is worse.
- **Communicate to team within one week.** After you have a plan. Honest, direct, with clarity on what it means for their jobs. "We have N months of runway. Here's what we're doing. Here's what this means for you."
### Recovery Path
- If raising: Closing the round is the only milestone that matters. Assign someone to own diligence data, legal docs, and investor follow-up. This is now the CEO's full-time job.
- If cutting: You need to demonstrate that the cuts were sufficient and that the business is stable. Three straight months of burn at or below plan is the proof point.
- The narrative question: "Why did this happen and why won't it happen again?" You will be asked this in the next fundraise. Have a direct, honest answer.
### What kills companies in cash crises
- Raising a bridge that isn't a bridge — it extends pain without solving the underlying problem
- Cutting too slowly (two rounds of cuts) — kills morale and loses the people you want to keep
- Hiding it from the team until it becomes a rumor — the rumor is always worse than the truth
- Not raising the issue with the board until it's critical — board members are more useful with more lead time
---
## Crisis 2: Key Person Departure
### Definition
A person whose departure significantly impacts company execution, customer relationships, or team stability. Usually C-level or a critical technical/commercial lead.
### First 24 Hours
- **Clarify what "departure" means.** Resignation? Fired? Mutual agreement? The situation determines the response.
- **Assess the actual impact.** What does this person own that isn't covered? Who on the team will be most affected? Do any customers have primary relationships with this person?
- **Secure institutional knowledge.** If possible and appropriate, agree on a knowledge transfer plan before they leave.
- **Notify the board chair.** Same day. Same rule: facts only, no spin.
- **Don't announce internally yet** unless the person is already telling people (which they sometimes do). Get ahead of it by a few hours if possible.
### First Week
- **Control the narrative internally.** All-hands or department meeting within 2–3 days. Honest: "Name is leaving. Here's what I can share about why. Here's the plan." Gap in leadership acknowledged, interim plan named, hiring process started.
- **Handle customer relationships.** Identify the top 5-10 customers with a relationship with this person. CEO or another senior person reaches out personally. "I want to make sure you hear from me directly..."
- **Announce interim ownership.** Don't leave reporting lines and responsibilities ambiguous. Even a temporary assignment provides stability.
- **Start the search.** Don't wait. The bench is always thinner than you think and searches take 3–4 months.
### Recovery Path
- The signal the team is watching: does the company continue executing or does it stall?
- Keep shipping. Keep hitting targets. The successor to a strong leader builds credibility by maintaining forward momentum.
- Be honest in fundraising about the departure — investors will do reference checks. "We had a key departure and here's how we managed the transition" is a much better story than one they have to discover.
---
## Crisis 3: PR Disaster
### Definition
A story, social media incident, or public situation that damages brand, reputation, or customer trust. Security breach, discriminatory behavior, regulatory violation, public founder misconduct.
### First 24 Hours
- **Establish facts before you communicate.** What actually happened? What data was affected? Who is affected? What is the extent?
- **Activate legal counsel immediately.** Before any external communication. Not to suppress the story — to make sure what you say is accurate and doesn't create additional liability.
- **Designate one spokesperson.** Only one person speaks to media, posts on social. Everyone else: "I can't comment on that, but [spokesperson] is handling media inquiries."
- **Acknowledge, don't stonewall.** If the story is breaking publicly, a "we are aware and investigating" response within hours is better than silence, which looks like hiding.
### First Week
- **Communicate to affected parties first.** If it's a data breach: affected customers before media. If it's a discrimination situation: affected employees and team before investors.
- **Draft a public statement.** Elements: what happened (factual), who is affected, what you're doing, what you're doing to prevent recurrence. No corporate-speak. No deflection. No passive voice ("mistakes were made").
- **Proactively update investors.** They'll hear about it anyway. Hearing from you first, with context, is materially better.
- **Execute the response plan.** Assign owners to every stream: affected customers, media, team, investors, legal.
### Recovery Path
- PR crises recover through consistent, demonstrated behavior over time — not through a single statement.
- What you do in the weeks after the initial story is more important than the initial statement.
- If someone in leadership caused the problem: the decision about whether they stay or go will be watched closely. Protecting the wrong person damages recovery.
- Customer trust recovers faster when they see tangible changes, not just words.
---
## Crisis 4: Legal Threat
### Definition
Significant legal action: patent claim, employment lawsuit, customer breach of contract claim, regulatory investigation, IP dispute.
### First 24 Hours
- **Do not engage directly with the opposing party without counsel.** Nothing — no calls, no emails, no messages.
- **Get legal counsel on the call today.** Not next week. If you have outside counsel, call them. If you don't have a relationship, get one immediately.
- **Document what you know.** The sequence of events, relevant contracts, communications. Don't delete or alter anything — that can become a separate problem.
- **Tell the board chair.** Same day. Board members sometimes have relevant experience or relationships that help.
### First Week
- **Assess exposure.** With counsel: what's the realistic worst case? What's the likely case? What's the cost range?
- **Determine response strategy.** Fight, settle, or ignore (only for clearly frivolous claims with no risk). Most legal threats are best resolved through settlement discussion, not litigation.
- **Evaluate business impact.** Does this affect fundraising? Customer relationships? Employment contracts? Scope the full impact.
- **Communication plan.** Employees? Customers? Investors? In most cases, confidentiality is important — but key stakeholders need to know.
### Recovery Path
- Most legal threats resolve. They resolve faster and cheaper when addressed directly and early.
- Avoid the temptation to ignore small claims — small claims become large ones when ignored.
- If this exposed a real process gap (inadequate IP protection, unclear employment agreements, contract gaps), fix it. The litigation is the signal; the underlying gap is the problem.
---
## Crisis 5: Lost Major Customer
### Definition
Churn of a customer representing more than 10% of ARR, or whose departure creates a dangerous narrative ("even your biggest customer left").
### First 24 Hours
- **Get the real reason.** Not the polite exit reason — the real one. Ask directly: "I want to understand what we could have done differently. Not to change the decision — to learn." Sometimes they'll tell you.
- **Assess financial impact.** Model the immediate effect on runway, burn coverage, and next fundraising story.
- **Notify the board chair.** If this is >10% ARR, same day. No surprises at board meeting.
- **Do not panic-announce internally.** You need a plan before you tell the team.
### First Week
- **Understand the signal.** Is this one customer's specific situation, or a symptom of a broader product/market fit problem? The answer changes the response completely.
- **Address the team.** The team will notice a major logo disappear. Name it, explain what you know, explain what's changing.
- **Accelerate pipeline.** If this creates a gap to target, which deals can be accelerated? What expansion opportunities are there with existing customers?
- **Review other at-risk customers.** Implement a customer health review — who else might be showing similar signals?
### Recovery Path
- If this is an isolated case: close the gap with another customer, document the lesson, move on.
- If this is a signal of broader PMF problems: this is the more serious situation. What are customers getting from you that they can't get elsewhere? Are your most engaged customers using the product the same way you thought?
- The fundraising question: "We lost [major customer]. Why?" Have a direct, honest answer that includes what you changed as a result.
---
## Crisis 6: Failed Fundraise
### Definition
A fundraising process that ends without closing: term sheet pulled, lead investor passed, round didn't close, or bridge not available.
### First 24 Hours
- **Assess actual runway.** How much time do you have at current burn?
- **Identify where the process broke.** Was it valuation? Team? Product? Market? The "why" determines the path.
- **Immediately convene board.** You need their help and their network. A failed raise is not something to manage quietly.
- **Do not tell the team yet.** You need a plan first. "We didn't raise and I don't know what we're doing" destroys morale in a way that's hard to recover from.
### First Week
- **Model survival scenarios.** At current burn: how long? At 50% reduced burn: how long? What does the reduced-burn company look like? Is it sustainable?
- **Identify specific reasons the raise failed.** Investor feedback, even if uncomfortable. "The market doesn't understand our vision" is not useful. "Three investors said the unit economics weren't believable" is useful.
- **Evaluate alternative paths.** Revenue-based financing, venture debt, strategic investment, customer advance payments, bridge from existing investors, acqui-hire.
- **Communicate to team.** Within one week. With a plan. "Here's what we're doing. Here's what this means for the team."
### Recovery Path
- The raise failed for reasons. Fix the reasons. If it was valuation: you may need to lower expectations. If it was market: you may need to refocus. If it was metrics: you need to improve metrics before the next attempt.
- Failed raises are more common than founders discuss publicly. Most companies that eventually succeed have had at least one.
- The companies that recover from failed fundraises usually do so by extending runway aggressively (cutting), finding a lead from outside their normal network, or changing something material about the business.
- **Do not do bridge rounds as avoidance.** A bridge that extends your runway 3 months to a problem you haven't fixed is not a solution. Only bridge if you have a specific, credible path to a successful close.
FILE:executive-mentor/references/hard_things.md
# Hard Things — Decision Frameworks for the Calls Nobody Wants to Make
Firing people. Laying off teams. Pivoting when you've raised money on the old direction. Telling a co-founder it's over. Shutting down a product.
This isn't a framework for feeling better about hard calls. It's a framework for making them correctly.
---
## Part 1: Firing
### When to Fire Someone
Most leaders wait too long. By the time they act, everyone else on the team already knows the problem person isn't working out. The team watches the leader, waiting to see if they'll act.
**Fire when:**
- Performance isn't improving after clear, direct, documented feedback
- The person is a culture or values problem, not just a skills problem
- You find yourself routing around them (giving their work to others, excluding them from important discussions)
- The team is being damaged by having them there
- You wouldn't hire them today for this role
**The question to ask:** "If I could wave a magic wand and this person just stopped coming to work, would I be relieved or would I miss them?" If relieved — you already know.
**The hidden test:** "Would I enthusiastically recommend this person to a friend's company for this exact role?" If no, what does that tell you?
### The Warning Signs You're Avoiding the Decision
- You've been "working on it" for more than 3 months
- You're hoping they'll leave on their own
- You're giving them feedback that's softer than what you actually think
- You're planning to "deal with it after the quarter"
- Other team members have started asking you about it
### Before Firing: The Due Diligence
Have you been **direct** — not hinted, not soft-pedaled, but explicitly said "your performance is not meeting the standard required for this role and your job is at risk"?
Have you given them **a fair chance to improve** with clear criteria for what success looks like?
Have you checked whether this is a **fit problem** (wrong role for their skills) vs a **performance problem** (not executing in a role they're capable of)?
Have you considered whether this is **your failure** — bad hire, bad onboarding, bad management — and whether another manager would get different results?
This isn't to talk yourself out of it. It's to make sure you can stand behind the decision.
### How to Fire Someone
**The conversation:**
Do it in person. Start of the week (not Friday — that's cruel). Private meeting. 30 minutes max.
Three sentences:
1. "I have difficult news — today is your last day."
2. "The reason is [one clear sentence — not a list of grievances]."
3. "Here's what the transition looks like [severance, references, timeline]."
**Do not:**
- Soften it so much that the person doesn't understand what's happening
- Give a performance review at the end ("you're really good at X but...")
- Apologize excessively (once is appropriate; more makes it about you)
- Leave open questions about whether this is final (it is)
**The question they'll ask:** "Why now?" Be ready for this. Have a direct answer.
**What to say to the team:** Same day. "I want to let you know that [Name] is no longer with the company. I can't share details, but I want to be transparent that this was a decision we made, not something they chose. Their last day is today." That's it. Don't litigate. Don't share reasons.
### Severance
Be generous. Not because you have to — because it's the right thing to do and it protects the culture. The team watches how you treat people when they leave.
For executives: 2–3 months standard, more if they've been there a long time.
For individual contributors: 2–4 weeks per year of service is reasonable.
**Reference:** Only confirm dates and title (standard practice). If you genuinely believe they'd be good somewhere else, offer a more substantive reference. Don't damage their career because the fit wasn't right.
---
## Part 2: Layoffs
### The First Question: Is This the Right Move?
Layoffs are sometimes the right call. But they're also sometimes an avoidance tactic — avoiding harder decisions about business model, spending discipline, or strategic direction.
Before proceeding, be clear on what problem you're solving:
- **Extending runway:** How many months does this buy? Is that enough?
- **Restructuring:** Are you changing the direction of the company, not just the headcount?
- **Cost cutting without strategic change:** This is usually a mistake — you lose talent, damage culture, and face the same problem 6 months later.
**The math:** At your current burn, you need to cut \_\_% to extend runway from \_\_ months to \_\_ months. That math should drive the decision, not a "feels about right" number.
### Cut Once, Cut Deep
The worst outcome is two rounds of layoffs. After the first, the people who stay are already thinking about leaving. A second round converts "scared" to "gone."
If you're going to do this, do it once and do it to a level that solves the problem for 18+ months. Psychological safety matters more than any individual cost saving.
### Deciding Who to Let Go
This is the hardest part. A framework:
**By role:** Does the company need this function at current stage? If you're cutting a whole team or capability, it's cleaner, more defensible, and recovers faster.
**By performance:** If cutting across teams, higher performers stay. This is the moment where the "we have no B players" culture claim is tested.
**By span of work:** Which work is critical path to the strategy you're executing now? Everything else is a candidate.
**The veto question:** "Would I fight to keep this person if they said they were leaving?" If yes, they're safe. If no, they're a candidate.
### The Layoff Conversation
**Preparation:**
- Legal review first. In Germany: Betriebsrat, social selection, proper notice periods. In the US: WARN Act for 50+ employees. Do not skip this.
- Have severance paperwork ready before the conversation
- Have IT ready to revoke access (dignity: after the conversation, not during)
**The conversation:**
- Private. Direct.
- "We're restructuring the company and your role is being eliminated."
- Don't blame the person. Don't say "we had to make hard choices" three times. Say it once and move on.
- Explain severance, timeline, references clearly.
- Answer questions. "I don't know" is acceptable for some questions. "I can't tell you" is not.
**All-hands same day:**
- You, live, as soon as individual conversations are done
- Be honest about why and what it means for the company
- Answer hard questions. Don't hide behind PR language.
- Acknowledge that this is hard and that you're responsible for the decisions that led here
### Survivor Guilt
The people who didn't get cut will feel: relieved, guilty, scared, and angry — often all four. Don't underestimate this.
Within 48 hours of the layoff:
- Talk to every team lead individually
- Hold a team meeting for each department
- Be available for hard conversations
The question everyone is silently asking: "Am I next?" Answer it directly, even if you can't promise the future: "I don't plan any further cuts. Here's what would have to be true for that to change."
---
## Part 3: Pivoting
### Signals That It's Time to Pivot
- Product-market fit isn't materializing despite iteration
- Growth requires heroic sales effort on every deal
- The customers who love you are not the customers you expected
- You find a problem you can solve well that's adjacent to what you're doing
- The market you targeted is smaller than you thought
**The danger signal:** You're pivoting to run from failure, not toward opportunity. Pivots pulled by evidence of a better path work. Pivots pushed by exhaustion with the current path fail differently.
### How to Think About the Pivot
Define what you're keeping vs. what you're changing:
- **Team**: usually keeping — the team is the asset
- **Technology**: partially keeping — usually can be reoriented
- **Customers**: depends — some will follow, some won't
- **Vision**: the long-term vision often survives; the near-term path changes
- **Brand**: sometimes requires a rename
The cleanest pivots have a clear answer to: "Why are we better positioned to win at the new thing than anyone else?"
### Telling the Board You're Pivoting
Do not surprise the board in a board meeting. Have the conversation individually with key directors first.
What to communicate:
1. What changed — the new data or insight that's driving this
2. What you're moving away from and why
3. What you're moving to and why you can win there
4. What this means for fundraising timeline and strategy
5. What you need from them
Board members hate two things: surprises and not being consulted. Give them both the information and the opportunity to contribute.
### Telling Customers You're Pivoting
Be direct. Don't spin it as "we're expanding our focus." If you're killing something they use, tell them clearly, with enough notice for them to plan.
What customers need to know:
- What's changing and when
- What happens to their data / integrations / workflows
- Who their contact is through the transition
- What alternatives exist
Customers who feel respected through a hard change sometimes become your biggest advocates. Customers who feel deceived become your loudest critics.
---
## Part 4: Co-Founder Conflicts
### The Types of Conflict
**Values/direction conflict:** You disagree fundamentally about what the company should be. This is existential and usually doesn't resolve with more conversation.
**Performance conflict:** One co-founder isn't pulling their weight. This is hard but more tractable — it's addressable with clarity.
**Role/scope conflict:** Unclear ownership causing friction. This is often fixable.
### The Conversation You're Not Having
Most co-founder conflicts fester because nobody says the real thing out loud.
The real thing might be: "I don't think you're growing into what this company needs." Or: "I don't agree with the direction you're pushing us and I don't feel heard." Or: "I'm doing 70% of the work and we have equal equity."
Say the real thing. Not in anger. Clearly, directly, with respect.
### When It's Not Working
Signs the co-founder relationship is unsalvageable:
- You've had the real conversation and nothing changed
- You don't trust their judgment anymore
- You've stopped including them in important decisions
- You're telling people (investors, team) a different story than what's true
- The team has started choosing sides
### The Separation
Options in rough order of impact:
1. **Role change** — they move to a different function where they can succeed
2. **Advisor role** — they step out of operations, keep some equity, maintain relationship
3. **Full exit** — they leave the company
For any separation: legal counsel first. Cap table, vesting, IP assignment, competition clauses — all need to be addressed. Don't make handshake deals.
How you treat the departing co-founder tells the team, the investors, and the market who you are.
---
## Part 5: Shutting Down a Product Line
### When to Kill It
- Revenue doesn't justify the cost (including the opportunity cost of what the team could be building instead)
- It's pulling the company in a strategic direction you're not committed to
- It requires resources disproportionate to its potential
- Supporting it is making the rest of the product worse
**The question to ask:** "If we launched this today knowing what we know, would we build it?" If no, that's your answer.
### What You're Protecting
The customers who use it. They trusted you with their workflow. Give them:
- Clear timeline (90 days minimum for anything with integration dependencies)
- Migration path to alternatives or your other products
- Data export
- A person they can contact with questions
### Internal Communication
The team that built it feels the loss personally. Acknowledge it. "This product represents real work and real care. Shutting it down is not a judgment of the team — it's a judgment about fit with where the company is going."
If team members are being reassigned, not let go — make that clear immediately. The fear of job loss will dominate every other concern until you address it.
FILE:executive-mentor/scripts/decision_matrix_scorer.py
#!/usr/bin/env python3
"""
Decision Matrix Scorer — Executive Mentor Tool
Weighted multi-criteria decision analysis with sensitivity testing.
Answers: Which option wins? How fragile is that result? Where are the close calls?
Usage:
python decision_matrix_scorer.py # Run with sample data
python decision_matrix_scorer.py --interactive # Interactive mode
python decision_matrix_scorer.py --file data.json # Load from JSON file
JSON format:
{
"decision": "Description of the decision",
"criteria": [
{"name": "Criterion Name", "weight": 0.3, "description": "Optional"},
...
],
"options": [
{
"name": "Option Name",
"description": "Optional description",
"scores": {"Criterion Name": 8, "Another": 6, ...}
},
...
]
}
Scores: 1–10 scale. Weights: must sum to 1.0 (or will be normalized).
"""
import json
import sys
import argparse
from typing import List, Dict, Tuple
# ─────────────────────────────────────────────────────
# Core data structures
# ─────────────────────────────────────────────────────
def normalize_weights(criteria: List[Dict]) -> List[Dict]:
"""Ensure weights sum to 1.0."""
total = sum(c["weight"] for c in criteria)
if abs(total - 1.0) > 0.001:
for c in criteria:
c["weight"] = c["weight"] / total
return criteria
def score_option(option: Dict, criteria: List[Dict]) -> float:
"""Calculate weighted score for an option."""
total = 0.0
for c in criteria:
score = option["scores"].get(c["name"], 5) # Default to 5 if missing
total += score * c["weight"]
return round(total, 3)
def score_all(options: List[Dict], criteria: List[Dict]) -> List[Tuple[str, float]]:
"""Return sorted list of (option_name, weighted_score)."""
results = []
for opt in options:
s = score_option(opt, criteria)
results.append((opt["name"], s))
return sorted(results, key=lambda x: x[1], reverse=True)
# ─────────────────────────────────────────────────────
# Sensitivity analysis
# ─────────────────────────────────────────────────────
def sensitivity_analysis(options: List[Dict], criteria: List[Dict]) -> Dict:
"""
Test how result changes when each criterion's weight is varied ±30%.
Returns dict: criterion → {stable: bool, risk_of_flip: bool, details: str}
"""
baseline = score_all(options, criteria)
winner = baseline[0][0]
results = {}
for i, c in enumerate(criteria):
flips = []
for delta in [-0.30, -0.20, -0.10, +0.10, +0.20, +0.30]:
# Adjust weight of criterion i, redistribute remainder proportionally
test_criteria = [dict(cr) for cr in criteria]
new_weight = max(0.01, test_criteria[i]["weight"] + delta)
old_weight = test_criteria[i]["weight"]
diff = new_weight - old_weight
# Redistribute diff across other criteria
others = [j for j in range(len(test_criteria)) if j != i]
total_other = sum(test_criteria[j]["weight"] for j in others)
if total_other > 0:
for j in others:
proportion = test_criteria[j]["weight"] / total_other
test_criteria[j]["weight"] -= diff * proportion
test_criteria[j]["weight"] = max(0.01, test_criteria[j]["weight"])
test_criteria[i]["weight"] = new_weight
test_criteria = normalize_weights(test_criteria)
test_results = score_all(options, test_criteria)
if test_results[0][0] != winner:
flips.append((delta, test_results[0][0]))
if flips:
smallest_delta = min(abs(delta) for delta, _name in flips)
results[c["name"]] = {
"stable": False,
"flip_at": f"±{int(smallest_delta*100)}% weight change",
"flip_to": flips[0][1],
"importance": "HIGH — result depends heavily on this weight"
}
else:
results[c["name"]] = {
"stable": True,
"flip_at": None,
"flip_to": None,
"importance": "LOW — winner holds even with significant weight changes"
}
return results
def close_call_analysis(results: List[Tuple[str, float]]) -> List[Dict]:
"""Find options within 10% of winner score — these are close calls."""
if not results:
return []
winner_score = results[0][1]
close = []
for name, score in results[1:]:
gap = winner_score - score
gap_pct = (gap / winner_score * 100) if winner_score > 0 else 0
if gap_pct <= 15:
close.append({
"name": name,
"score": score,
"gap": round(gap, 3),
"gap_pct": round(gap_pct, 1),
"verdict": "Very close — recheck assumptions" if gap_pct <= 5 else "Close — worth a second look"
})
return close
def criterion_breakdown(options: List[Dict], criteria: List[Dict]) -> Dict:
"""Show per-criterion scores for each option."""
breakdown = {}
for opt in options:
breakdown[opt["name"]] = {}
for c in criteria:
raw = opt["scores"].get(c["name"], 5)
weighted = raw * c["weight"]
breakdown[opt["name"]][c["name"]] = {
"raw": raw,
"weighted": round(weighted, 3),
"weight": f"{round(c['weight']*100)}%"
}
return breakdown
# ─────────────────────────────────────────────────────
# Output formatting
# ─────────────────────────────────────────────────────
def hr(char="─", width=65):
return char * width
def print_report(data: Dict):
"""Print the full decision analysis report."""
decision = data.get("decision", "Unnamed Decision")
criteria = normalize_weights(data["criteria"])
options = data["options"]
print()
print(hr("═"))
print(f" DECISION MATRIX ANALYSIS")
print(f" {decision}")
print(hr("═"))
# ── Criteria summary
print()
print("CRITERIA & WEIGHTS")
print(hr())
for c in sorted(criteria, key=lambda x: x["weight"], reverse=True):
bar_len = int(c["weight"] * 30)
bar = "█" * bar_len
desc = f" — {c['description']}" if c.get("description") else ""
print(f" {c['name']:<25} {c['weight']*100:>5.1f}% {bar}{desc}")
# ── Scoring results
print()
print("RESULTS (ranked)")
print(hr())
results = score_all(options, criteria)
max_score = 10.0 # max possible weighted score
for rank, (name, score) in enumerate(results, 1):
pct = score / 10.0
bar_len = int(pct * 40)
bar = "█" * bar_len
medal = ["🥇", "🥈", "🥉"][rank-1] if rank <= 3 else f"#{rank} "
print(f" {medal} {name:<25} {score:>5.2f}/10 {bar}")
winner = results[0][0]
print()
print(f" ► Winner: {winner} (score: {results[0][1]:.2f})")
# ── Close calls
close = close_call_analysis(results)
if close:
print()
print("CLOSE CALLS")
print(hr())
for c in close:
print(f" ⚠ {c['name']}: {c['score']:.2f} (gap: {c['gap_pct']}% — {c['verdict']})")
# ── Per-criterion breakdown
print()
print("SCORE BREAKDOWN BY CRITERION")
print(hr())
breakdown = criterion_breakdown(options, criteria)
# Header
opt_names = [opt["name"][:16] for opt in options]
header = f" {'Criterion':<22}"
for n in opt_names:
header += f" {n:>10}"
print(header)
print(" " + hr("-", 63))
for c in criteria:
row = f" {c['name']:<22}"
for opt in options:
raw = opt["scores"].get(c["name"], 5)
row += f" {raw:>10}"
row += f" (weight {c['weight']*100:.0f}%)"
print(row)
# Weighted row
print(" " + hr("-", 63))
weighted_row = f" {'Weighted Total':<22}"
for name, score in results:
# Re-order by options list order
weighted_row += f" {score:>10.2f}"
# Actually print in options order
print(f" {'Weighted Total':<22}", end="")
for opt in options:
s = score_option(opt, criteria)
print(f" {s:>10.2f}", end="")
print()
# ── Sensitivity analysis
print()
print("SENSITIVITY ANALYSIS")
print(hr())
print(" How much does the winner change if we adjust criterion weights?")
print()
sensitivity = sensitivity_analysis(options, criteria)
for crit_name, result in sensitivity.items():
if result["stable"]:
print(f" ✓ {crit_name:<28} STABLE — winner holds at ±30% weight change")
else:
print(f" ⚠ {crit_name:<28} FRAGILE — flips to '{result['flip_to']}' at {result['flip_at']}")
# ── Recommendation
print()
print("RECOMMENDATION")
print(hr())
unstable = [k for k, v in sensitivity.items() if not v["stable"]]
if unstable:
print(f" Winner: {winner}")
print(f" Confidence: MEDIUM — result is sensitive to weights on: {', '.join(unstable)}")
print()
print(" Before committing:")
print(f" • Validate that your weighting of [{', '.join(unstable)}] is correct")
print(" • Consider whether the weight differences reflect genuine priorities")
print(" • If uncertain, run scenario with alternative weights")
else:
print(f" Winner: {winner}")
print(f" Confidence: HIGH — winner is stable across all weight scenarios")
print()
print(" The decision is clear. The main risk is whether your scoring")
print(" of each option on each criterion is accurate.")
print()
print(hr("═"))
print()
# ─────────────────────────────────────────────────────
# Interactive mode
# ─────────────────────────────────────────────────────
def interactive_mode():
"""Guided interactive data entry."""
print()
print(hr("═"))
print(" DECISION MATRIX — Interactive Mode")
print(hr("═"))
data = {}
data["decision"] = input("\nWhat decision are you making?\n> ").strip()
# Criteria
print("\nDefine criteria (what matters in this decision).")
print("Enter criteria one at a time. Empty line to finish.")
print("Weight: importance 0–10 (will be normalized to %).")
print()
criteria = []
while True:
name = input(f"Criterion {len(criteria)+1} name (or ENTER to finish): ").strip()
if not name:
if len(criteria) < 2:
print(" Need at least 2 criteria.")
continue
break
weight_str = input(f" Weight for '{name}' (0–10): ").strip()
try:
weight = float(weight_str)
except ValueError:
weight = 5.0
criteria.append({"name": name, "weight": weight})
data["criteria"] = criteria
# Options
print("\nDefine options (what you're choosing between).")
print("Enter options one at a time. Empty line to finish.")
print()
options = []
while True:
name = input(f"Option {len(options)+1} name (or ENTER to finish): ").strip()
if not name:
if len(options) < 2:
print(" Need at least 2 options.")
continue
break
print(f"\n Score each criterion for '{name}' (1=poor, 10=excellent):")
scores = {}
for c in criteria:
while True:
s = input(f" {c['name']}: ").strip()
try:
score = float(s)
if 1 <= score <= 10:
scores[c["name"]] = score
break
else:
print(" Score must be 1–10")
except ValueError:
print(" Enter a number 1–10")
options.append({"name": name, "scores": scores})
print()
data["options"] = options
print_report(data)
# ─────────────────────────────────────────────────────
# Sample data
# ─────────────────────────────────────────────────────
SAMPLE_DATA = {
"decision": "How to extend runway: Cut costs vs. Raise bridge vs. Accelerate revenue",
"criteria": [
{
"name": "Speed to impact",
"weight": 0.25,
"description": "How quickly does this improve our situation?"
},
{
"name": "Execution risk",
"weight": 0.30,
"description": "How likely is this to actually work? (10=low risk)"
},
{
"name": "Team morale impact",
"weight": 0.20,
"description": "Effect on team (10=positive, 1=very negative)"
},
{
"name": "Runway extension",
"weight": 0.15,
"description": "How much runway does this actually buy?"
},
{
"name": "Strategic fit",
"weight": 0.10,
"description": "Does this align with where we want to go?"
}
],
"options": [
{
"name": "Cost cut 25%",
"description": "Reduce headcount and discretionary spend by 25%",
"scores": {
"Speed to impact": 9,
"Execution risk": 8,
"Team morale impact": 2,
"Runway extension": 8,
"Strategic fit": 5
}
},
{
"name": "Bridge from investors",
"description": "Raise $500K bridge from existing investors to hit next milestone",
"scores": {
"Speed to impact": 6,
"Execution risk": 5,
"Team morale impact": 7,
"Runway extension": 6,
"Strategic fit": 7
}
},
{
"name": "Accelerate revenue",
"description": "Push 3 enterprise deals hard, offer incentives for Q4 close",
"scores": {
"Speed to impact": 4,
"Execution risk": 3,
"Team morale impact": 9,
"Runway extension": 9,
"Strategic fit": 10
}
},
{
"name": "Hybrid: cut 15% + bridge",
"description": "Smaller cuts combined with a modest bridge round",
"scores": {
"Speed to impact": 7,
"Execution risk": 6,
"Team morale impact": 5,
"Runway extension": 7,
"Strategic fit": 6
}
}
]
}
# ─────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Decision Matrix Scorer — weighted analysis with sensitivity testing"
)
parser.add_argument(
"--interactive", "-i",
action="store_true",
help="Interactive mode: enter decision data manually"
)
parser.add_argument(
"--file", "-f",
type=str,
help="Load decision data from JSON file"
)
parser.add_argument(
"--sample",
action="store_true",
help="Show sample data structure and exit"
)
args = parser.parse_args()
if args.sample:
print(json.dumps(SAMPLE_DATA, indent=2))
return
if args.interactive:
interactive_mode()
return
if args.file:
try:
with open(args.file) as f:
data = json.load(f)
print_report(data)
except FileNotFoundError:
print(f"Error: File '{args.file}' not found.")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.file}': {e}")
sys.exit(1)
return
# Default: run sample data
print()
print("Running with sample data. Use --interactive for custom input or --file for JSON.")
print_report(SAMPLE_DATA)
if __name__ == "__main__":
main()
FILE:executive-mentor/scripts/stakeholder_mapper.py
#!/usr/bin/env python3
"""
Stakeholder Mapper — Executive Mentor Tool
Maps stakeholders by influence and alignment.
Identifies: champions, blockers, swing votes, and hidden risks.
Outputs: stakeholder grid with engagement strategy per quadrant.
Usage:
python stakeholder_mapper.py # Run with sample data
python stakeholder_mapper.py --interactive # Interactive mode
python stakeholder_mapper.py --file data.json # Load from JSON file
JSON format:
{
"initiative": "Name of the decision or initiative",
"stakeholders": [
{
"name": "Person/Group Name",
"role": "Their role or title",
"influence": 8, // 1–10: how much power they have over outcome
"alignment": 3, // 1–10: how supportive they are (10=champion, 1=blocker)
"interest": 7, // 1–10: how interested/engaged they are
"notes": "Optional context — what drives them, hidden concerns, relationships"
}
]
}
"""
import json
import sys
import argparse
from typing import List, Dict, Tuple, Optional
# ─────────────────────────────────────────────────────
# Quadrant classification
# ─────────────────────────────────────────────────────
def classify_stakeholder(influence: float, alignment: float) -> Dict:
"""
Classify into strategic quadrant based on influence and alignment.
Quadrants:
- Champions (high influence, high alignment): Your most valuable assets
- Blockers (high influence, low alignment): Your biggest risks
- Supporters (low influence, high alignment): Useful but less critical
- Bystanders (low influence, low alignment): Monitor, low priority
- Swing Votes (medium influence, medium alignment): Key to persuade
"""
mid_influence = 5.5
mid_alignment = 5.5
# Special case: swing votes — medium on both dimensions
if 4 <= influence <= 7 and 4 <= alignment <= 7:
return {
"quadrant": "Swing Vote",
"symbol": "⚡",
"priority": "HIGH",
"strategy": "Persuade — understand concerns, address directly, build relationship"
}
if influence >= mid_influence and alignment >= mid_alignment:
return {
"quadrant": "Champion",
"symbol": "★",
"priority": "HIGH",
"strategy": "Leverage — activate them as advocates, give them a role in the initiative"
}
elif influence >= mid_influence and alignment < mid_alignment:
return {
"quadrant": "Blocker",
"symbol": "✖",
"priority": "CRITICAL",
"strategy": "Address — understand their specific objections, find common ground or neutralize"
}
elif influence < mid_influence and alignment >= mid_alignment:
return {
"quadrant": "Supporter",
"symbol": "○",
"priority": "MEDIUM",
"strategy": "Maintain — keep informed and engaged, potentially increase their influence"
}
else:
return {
"quadrant": "Bystander",
"symbol": "·",
"priority": "LOW",
"strategy": "Monitor — minimal investment, keep informed with standard comms"
}
def risk_flags(stakeholder: Dict) -> List[str]:
"""Identify specific risk signals for a stakeholder."""
flags = []
influence = stakeholder["influence"]
alignment = stakeholder["alignment"]
interest = stakeholder.get("interest", 5)
if influence >= 7 and alignment <= 3:
flags.append("🔴 HIGH-POWER BLOCKER — can kill this initiative")
if influence >= 7 and alignment <= 5 and interest >= 7:
flags.append("🟡 ENGAGED SKEPTIC — high influence, paying close attention, not convinced")
if alignment <= 4 and interest >= 8:
flags.append("🟡 ACTIVE OPPOSITION — low alignment but highly engaged — may mobilize others")
if influence >= 6 and alignment >= 7 and interest <= 3:
flags.append("🟡 DISENGAGED CHAMPION — strong supporter but not paying attention — needs activation")
if influence >= 5 and 4 <= alignment <= 6:
flags.append("⚡ PERSUADABLE — medium influence, genuinely undecided — high ROI to engage")
return flags
# ─────────────────────────────────────────────────────
# Analysis
# ─────────────────────────────────────────────────────
def calculate_overall_alignment(stakeholders: List[Dict]) -> Dict:
"""Calculate weighted average alignment (weighted by influence)."""
if not stakeholders:
return {"score": 0, "verdict": "No data"}
total_influence = sum(s["influence"] for s in stakeholders)
if total_influence == 0:
return {"score": 0, "verdict": "No influence"}
weighted_alignment = sum(
s["alignment"] * s["influence"] for s in stakeholders
) / total_influence
if weighted_alignment >= 7:
verdict = "FAVORABLE — strong support among influential stakeholders"
elif weighted_alignment >= 5:
verdict = "MIXED — significant opposition needs to be addressed"
else:
verdict = "UNFAVORABLE — initiative faces significant headwinds"
return {
"score": round(weighted_alignment, 2),
"verdict": verdict
}
def find_critical_path(stakeholders: List[Dict]) -> List[Dict]:
"""
Identify the minimal set of stakeholders whose alignment is critical.
These are high-influence stakeholders — their position determines the outcome.
"""
high_influence = [s for s in stakeholders if s["influence"] >= 7]
return sorted(high_influence, key=lambda x: x["influence"], reverse=True)
def engagement_sequencing(stakeholders: List[Dict]) -> List[Dict]:
"""
Recommend engagement sequence.
Order: Fix blockers → Activate champions → Persuade swing votes → Maintain rest.
"""
classified = []
for s in stakeholders:
cls = classify_stakeholder(s["influence"], s["alignment"])
classified.append({**s, **cls})
# Sort by engagement priority
priority_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
classified.sort(key=lambda x: (priority_order[x["priority"]], -x["influence"]))
return classified
# ─────────────────────────────────────────────────────
# ASCII grid visualization
# ─────────────────────────────────────────────────────
def render_grid(stakeholders: List[Dict], width: int = 60) -> str:
"""
Render a 2D influence vs alignment grid with stakeholder positions.
Y-axis: Influence (top = high)
X-axis: Alignment (left = low, right = high)
"""
rows = 10
cols = 20
grid = [[' ' for _ in range(cols)] for _ in range(rows)]
for s in stakeholders:
influence = s["influence"]
alignment = s["alignment"]
# Map scores 1–10 to grid coordinates
col = int((alignment - 1) / 9 * (cols - 1))
row = rows - 1 - int((influence - 1) / 9 * (rows - 1))
col = max(0, min(cols - 1, col))
row = max(0, min(rows - 1, row))
initial = s["name"][0].upper()
if grid[row][col] == ' ':
grid[row][col] = initial
else:
grid[row][col] = '+' # Overlap
lines = []
lines.append(" STAKEHOLDER MAP (Influence ↑ | Alignment →)")
lines.append("")
lines.append(f" HIGH ┌{'─'*cols}┐")
for i, row in enumerate(grid):
if i == rows // 2:
prefix = " INFL "
else:
prefix = " "
lines.append(f"{prefix}│{''.join(row)}│")
lines.append(f" LOW └{'─'*cols}┘")
lines.append(f" {'BLOCKER':<12} {'SWING':<8} CHAMPION")
lines.append(f" Low alignment High alignment")
lines.append("")
# Legend
lines.append(" Legend (initials):")
for s in stakeholders:
cls = classify_stakeholder(s["influence"], s["alignment"])
lines.append(f" {s['name'][0].upper()} = {s['name']} ({cls['symbol']} {cls['quadrant']})")
return "\n".join(lines)
# ─────────────────────────────────────────────────────
# Output formatting
# ─────────────────────────────────────────────────────
def hr(char="─", width=65):
return char * width
def print_report(data: Dict):
initiative = data.get("initiative", "Unnamed Initiative")
stakeholders = data["stakeholders"]
# Validate and fill defaults
for s in stakeholders:
s.setdefault("interest", 5)
s.setdefault("notes", "")
s["influence"] = max(1, min(10, float(s["influence"])))
s["alignment"] = max(1, min(10, float(s["alignment"])))
s["interest"] = max(1, min(10, float(s["interest"])))
print()
print(hr("═"))
print(f" STAKEHOLDER ANALYSIS")
print(f" {initiative}")
print(hr("═"))
# Overall assessment
overall = calculate_overall_alignment(stakeholders)
print()
print("OVERALL ASSESSMENT")
print(hr())
print(f" Weighted alignment score: {overall['score']}/10")
print(f" Verdict: {overall['verdict']}")
# Grid visualization
print()
print(hr())
print(render_grid(stakeholders))
# Stakeholder profiles by quadrant
sequenced = engagement_sequencing(stakeholders)
# Group by quadrant
quadrants = {}
for s in sequenced:
q = s["quadrant"]
if q not in quadrants:
quadrants[q] = []
quadrants[q].append(s)
quadrant_order = ["Blocker", "Swing Vote", "Champion", "Supporter", "Bystander"]
print()
print("STAKEHOLDER PROFILES")
print(hr())
for q_name in quadrant_order:
if q_name not in quadrants:
continue
group = quadrants[q_name]
first = group[0]
print()
print(f" {first['symbol']} {q_name.upper()}S ({len(group)} stakeholder{'s' if len(group)>1 else ''})")
print(f" Strategy: {first['strategy']}")
print()
for s in group:
cls = classify_stakeholder(s["influence"], s["alignment"])
flags = risk_flags(s)
print(f" {s['name']}")
print(f" Role: {s.get('role', 'Not specified')}")
print(f" Influence: {'█'*int(s['influence']//2)}{'░'*(5-int(s['influence']//2))} {s['influence']:.0f}/10 "
f"Alignment: {'█'*int(s['alignment']//2)}{'░'*(5-int(s['alignment']//2))} {s['alignment']:.0f}/10 "
f"Interest: {'█'*int(s['interest']//2)}{'░'*(5-int(s['interest']//2))} {s['interest']:.0f}/10")
if flags:
for flag in flags:
print(f" {flag}")
if s.get("notes"):
print(f" Notes: {s['notes']}")
print()
# Engagement plan
print()
print("ENGAGEMENT PLAN (sequenced by priority)")
print(hr())
print()
print(f" {'#':<3} {'Name':<22} {'Quadrant':<14} {'Priority':<10} {'First Action'}")
print(f" {hr('-', 63)}")
actions = {
"Blocker": "Schedule 1:1 — understand specific objections",
"Swing Vote": "Coffee or informal conversation — listen first",
"Champion": "Brief them on the initiative — give them a role",
"Supporter": "Keep informed — monthly update or email",
"Bystander": "Include in standard comms only"
}
for i, s in enumerate(sequenced, 1):
action = actions.get(s["quadrant"], "Maintain standard communication")
print(f" {i:<3} {s['name']:<22} {s['quadrant']:<14} {s['priority']:<10} {action}")
# Risk summary
print()
print("RISK SUMMARY")
print(hr())
critical_path = find_critical_path(stakeholders)
if critical_path:
print()
print(" High-influence stakeholders (outcome depends on these):")
for s in critical_path:
cls = classify_stakeholder(s["influence"], s["alignment"])
alignment_label = "CHAMPION" if s["alignment"] >= 7 else "BLOCKER" if s["alignment"] <= 4 else "UNDECIDED"
print(f" {cls['symbol']} {s['name']:<25} influence {s['influence']:.0f}/10 → {alignment_label}")
# All risk flags
all_flags = []
for s in stakeholders:
flags = risk_flags(s)
for flag in flags:
all_flags.append((s["name"], flag))
if all_flags:
print()
print(" Risk flags:")
for name, flag in all_flags:
print(f" [{name}] {flag}")
print()
print(hr("═"))
print()
# ─────────────────────────────────────────────────────
# Interactive mode
# ─────────────────────────────────────────────────────
def interactive_mode():
print()
print(hr("═"))
print(" STAKEHOLDER MAPPER — Interactive Mode")
print(hr("═"))
data = {}
data["initiative"] = input("\nWhat initiative or decision are you mapping?\n> ").strip()
print("\nAdd stakeholders one at a time. Empty name to finish.")
print("Scores: 1=low, 10=high")
print()
stakeholders = []
while True:
name = input(f"Stakeholder {len(stakeholders)+1} name (or ENTER to finish): ").strip()
if not name:
if len(stakeholders) < 1:
print(" Need at least 1 stakeholder.")
continue
break
role = input(f" Role/title: ").strip()
def get_score(prompt, default=5):
while True:
s = input(f" {prompt} (1–10, default {default}): ").strip()
if not s:
return float(default)
try:
v = float(s)
if 1 <= v <= 10:
return v
print(" Must be 1–10")
except ValueError:
print(" Enter a number")
influence = get_score("Influence (power over this decision)")
alignment = get_score("Alignment (1=opposed, 10=champion)")
interest = get_score("Interest level (how engaged are they)")
notes = input(f" Notes (optional): ").strip()
stakeholders.append({
"name": name,
"role": role,
"influence": influence,
"alignment": alignment,
"interest": interest,
"notes": notes
})
print()
data["stakeholders"] = stakeholders
print_report(data)
# ─────────────────────────────────────────────────────
# Sample data
# ─────────────────────────────────────────────────────
SAMPLE_DATA = {
"initiative": "Migrate from monolith to microservices (18-month program)",
"stakeholders": [
{
"name": "Sarah Chen (CTO)",
"role": "Chief Technology Officer",
"influence": 10,
"alignment": 9,
"interest": 9,
"notes": "Driving force behind the initiative. Will fund and protect the team."
},
{
"name": "Marcus Webb (CFO)",
"role": "Chief Financial Officer",
"influence": 9,
"alignment": 3,
"interest": 6,
"notes": "Concerned about 18-month cost with no visible revenue return. Has budget veto."
},
{
"name": "Priya Agarwal (VP Eng)",
"role": "VP Engineering",
"influence": 8,
"alignment": 7,
"interest": 8,
"notes": "Supportive in principle, worried about team bandwidth alongside feature delivery."
},
{
"name": "Tom Briggs (VP Product)",
"role": "VP Product",
"influence": 7,
"alignment": 4,
"interest": 5,
"notes": "Concerned about roadmap slowdown. Hasn't been in the architecture discussions."
},
{
"name": "Elena Park (CEO)",
"role": "Chief Executive Officer",
"influence": 10,
"alignment": 6,
"interest": 4,
"notes": "Trusts the CTO but will back out if CFO and VP Product both push back hard."
},
{
"name": "Raj Patel (Lead Arch)",
"role": "Lead Architect",
"influence": 6,
"alignment": 10,
"interest": 10,
"notes": "Deep technical champion. Has proposed detailed migration plan."
},
{
"name": "Dev Team Leads (x4)",
"role": "Team Leads",
"influence": 5,
"alignment": 6,
"interest": 7,
"notes": "Mixed. Some excited, some worried about learning curve. Middle ground."
},
{
"name": "Board (investor reps)",
"role": "Board Directors",
"influence": 9,
"alignment": 5,
"interest": 3,
"notes": "Not paying attention unless CFO raises flags. Could become blockers if CFO escalates."
}
]
}
# ─────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Stakeholder Mapper — influence, alignment, and engagement strategy"
)
parser.add_argument(
"--interactive", "-i",
action="store_true",
help="Interactive mode: enter stakeholder data manually"
)
parser.add_argument(
"--file", "-f",
type=str,
help="Load stakeholder data from JSON file"
)
parser.add_argument(
"--sample",
action="store_true",
help="Print sample JSON structure and exit"
)
args = parser.parse_args()
if args.sample:
print(json.dumps(SAMPLE_DATA, indent=2))
return
if args.interactive:
interactive_mode()
return
if args.file:
try:
with open(args.file) as f:
data = json.load(f)
print_report(data)
except FileNotFoundError:
print(f"Error: File '{args.file}' not found.")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.file}': {e}")
sys.exit(1)
return
# Default: sample data
print()
print("Running with sample data. Use --interactive for custom input or --file for JSON.")
print_report(SAMPLE_DATA)
if __name__ == "__main__":
main()
FILE:executive-mentor/skills/board-prep/SKILL.md
---
name: "board-prep"
description: "/em -board-prep — Board Meeting Preparation"
---
# /em:board-prep — Board Meeting Preparation
**Command:** `/em:board-prep <agenda>`
Prepare for the adversarial version of your board, not the friendly one. Every hard question they'll ask. Every number you need cold. The narrative that acknowledges weakness without losing the room.
---
## The Reality of Board Meetings
Your board members have seen 50+ companies. They've watched founders flinch at their own numbers, spin bad news as "learning opportunities," and present sanitized decks that hide what's actually happening.
They know when you're not being straight with them. The question isn't whether they'll ask the hard questions — it's whether you're ready for them.
The best board meetings aren't the ones where everything looks good. They're the ones where the CEO demonstrates they see reality clearly, have a plan, and can execute under pressure.
---
## The Preparation Framework
### Phase 1: Numbers Cold
Before the meeting, every number in your deck should live in your head, not just the slide.
**The numbers you must know without looking:**
- Current MRR / ARR and month-over-month growth rate
- Burn rate (monthly) and runway (months at current burn)
- Headcount by department
- CAC and LTV by channel / segment
- Net Revenue Retention
- Pipeline: value, conversion rate, average sales cycle
- Churn: rate, top reasons, top churned accounts
- Gross margin (product), net margin (company)
- Key hiring positions open and time-to-fill
**Stress test yourself:** Can you answer "what's your burn?" without hesitation? "What's your churn rate by segment?" If you pause, you don't know it.
### Phase 2: Anticipate the Hard Questions
For every item on the agenda, generate the adversarial version of the question.
**Standard adversarial questions by topic:**
*Revenue performance:*
- "You missed revenue by 20% this quarter. What specifically failed?"
- "Is this a pipeline problem, a conversion problem, or a capacity problem?"
- "If you missed because of one big deal, how dependent is your model on individual deals?"
- "When do you project recovery and what are the leading indicators you're right?"
*Runway / burn:*
- "At current burn you have N months. What's your plan if the next round takes 9 months?"
- "What would you cut first if you had to extend runway by 6 months today?"
- "Is there a scenario where you don't raise another round?"
*Product / roadmap:*
- "You shipped X. What did customers actually do with it?"
- "What did you kill this quarter and why?"
- "Where are you behind on roadmap? What's slipping?"
*Team:*
- "Who's at risk of leaving? How would that affect execution?"
- "You've had 3 VP-level hires not work out. What pattern do you see?"
- "Is the team the right team for this stage?"
*Competition:*
- "Competitor Y just raised $50M. How does that change your position?"
- "If they copy your best feature in 90 days, what's your moat?"
### Phase 3: Build the Narrative
The board meeting isn't a status update. It's a leadership demonstration.
**The structure that works:**
1. **Where we are (honest)** — Current state of business, the real number, not the smoothed one
2. **What we learned** — What the data is telling us that we didn't know 90 days ago
3. **What we got wrong** — Name it directly. Don't make them ask.
4. **What we're doing about it** — Specific, dated, owned actions
5. **What we need from this room** — Concrete ask. Not "support" — specific introductions, decisions, resources.
**The rule on bad news:** Never let the board be surprised. If a quarter went badly, they should know before the deck. A 5-sentence email 3 days before: "Revenue came in at $X vs $Y target. Here's what happened, here's what I'm doing, here's what I need from you."
### Phase 4: Adversarial Preparation
Do a mock board meeting. Have someone play the hardest director you have.
**The simulation:**
- Present your deck as you would
- The mock director asks every uncomfortable question
- You answer without referring to the deck
- After: note every question that made you pause or feel defensive
**The questions that made you defensive = the questions you need to prepare for.**
### Phase 5: Director-by-Director Prep
Not all board members want the same thing from a meeting.
**For each director, know:**
- Their primary concern right now (usually tied to their investment thesis)
- The metric they watch most closely
- What would make them lose confidence in you
- What they've said in the last meeting that you should address
**Common director types:**
- **The operator** — wants to know what's breaking and who owns fixing it
- **The financial investor** — focused on path to profitability or next raise
- **The strategic investor** — worried about competitive position and moat
- **The independent** — watching governance, team dynamics, and your judgment
---
## Pre-Meeting Checklist
**48 hours before:**
- [ ] All numbers verified against source systems (not last week's export)
- [ ] Deck reviewed for internal consistency
- [ ] Pre-read sent to board (deck + 1-page brief on key topics)
- [ ] One-on-ones done with any director likely to have concerns
- [ ] 3 hardest questions you expect — rehearsed out loud
**Day of meeting:**
- [ ] Agenda with time allocations distributed
- [ ] Know the ask for each agenda item (decision needed, input wanted, FYI)
- [ ] Materials to leave behind prepared
- [ ] Follow-up action template ready
---
## During the Meeting
**What the board is watching:**
- Do you own the bad news or deflect it?
- Are you defending a narrative or sharing reality?
- Do you know your numbers or do you look things up?
- When challenged, do you get defensive or engage?
- Do you know what you don't know?
**The single best thing you can do:** Name the hard thing before they do. "I want to address the revenue miss directly. Here's what happened, here's what I should have caught earlier, here's what changes."
---
## After the Meeting
Within 24 hours:
- Send action items with owners and dates
- Send any data you promised but didn't have
- Note the questions that came up you weren't ready for
- Schedule follow-up with any director who seemed unsatisfied
The next board prep starts now.
FILE:executive-mentor/skills/challenge/SKILL.md
---
name: "challenge"
description: "/em -challenge — Pre-Mortem Plan Analysis"
---
# /em:challenge — Pre-Mortem Plan Analysis
**Command:** `/em:challenge <plan>`
Systematically finds weaknesses in any plan before reality does. Not to kill the plan — to make it survive contact with reality.
---
## The Core Idea
Most plans fail for predictable reasons. Not bad luck — bad assumptions. Overestimated demand. Underestimated complexity. Dependencies nobody questioned. Timing that made sense in a spreadsheet but not in the real world.
The pre-mortem technique: **imagine it's 12 months from now and this plan failed spectacularly. Now work backwards. Why?**
That's not pessimism. It's how you build something that doesn't collapse.
---
## When to Run a Challenge
- Before committing significant resources to a plan
- Before presenting to the board or investors
- When you notice you're only hearing positive feedback about the plan
- When the plan requires multiple external dependencies to align
- When there's pressure to move fast and "figure it out later"
- When you feel excited about the plan (excitement is a signal to scrutinize harder)
---
## The Challenge Framework
### Step 1: Extract Core Assumptions
Before you can test a plan, you need to surface everything it assumes to be true.
For each section of the plan, ask:
- What has to be true for this to work?
- What are we assuming about customer behavior?
- What are we assuming about competitor response?
- What are we assuming about our own execution capability?
- What external factors does this depend on?
**Common assumption categories:**
- **Market assumptions** — size, growth rate, customer willingness to pay, buying cycle
- **Execution assumptions** — team capacity, velocity, no major hires needed
- **Customer assumptions** — they have the problem, they know they have it, they'll pay to solve it
- **Competitive assumptions** — incumbents won't respond, no new entrant, moat holds
- **Financial assumptions** — burn rate, revenue timing, CAC, LTV ratios
- **Dependency assumptions** — partner will deliver, API won't change, regulations won't shift
### Step 2: Rate Each Assumption
For every assumption extracted, rate it on two dimensions:
**Confidence level (how sure are you this is true):**
- **High** — verified with data, customer conversations, market research
- **Medium** — directionally right but not validated
- **Low** — plausible but untested
- **Unknown** — we simply don't know
**Impact if wrong (what happens if this assumption fails):**
- **Critical** — plan fails entirely
- **High** — major delay or cost overrun
- **Medium** — significant rework required
- **Low** — manageable adjustment
### Step 3: Map Vulnerabilities
The matrix of Low/Unknown confidence × Critical/High impact = your highest-risk assumptions.
**Vulnerability = Low confidence + High impact**
These are not problems to ignore. They're the bets you're making. The question is: are you making them consciously?
### Step 4: Find the Dependency Chain
Many plans fail not because any single assumption is wrong, but because multiple assumptions have to be right simultaneously.
Map the chain:
- Does assumption B depend on assumption A being true first?
- If the first thing goes wrong, how many downstream things break?
- What's the critical path? What has zero slack?
### Step 5: Test the Reversibility
For each critical vulnerability: if this assumption turns out to be wrong at month 3, what do you do?
- Can you pivot?
- Can you cut scope?
- Is money already spent?
- Are commitments already made?
The less reversible, the more rigorously you need to validate before committing.
---
## Output Format
**Challenge Report: [Plan Name]**
```
CORE ASSUMPTIONS (extracted)
1. [Assumption] — Confidence: [H/M/L/?] — Impact if wrong: [Critical/High/Medium/Low]
2. ...
VULNERABILITY MAP
Critical risks (act before proceeding):
• [#N] [Assumption] — WHY it might be wrong — WHAT breaks if it is
High risks (validate before scaling):
• ...
DEPENDENCY CHAIN
[Assumption A] → depends on → [Assumption B] → which enables → [Assumption C]
Weakest link: [X] — if this breaks, [Y] and [Z] also fail
REVERSIBILITY ASSESSMENT
• Reversible bets: [list]
• Irreversible commitments: [list — treat with extreme care]
KILL SWITCHES
What would have to be true at [30/60/90 days] to continue vs. kill/pivot?
• Continue if: ...
• Kill/pivot if: ...
HARDENING ACTIONS
1. [Specific validation to do before proceeding]
2. [Alternative approach to consider]
3. [Contingency to build into the plan]
```
---
## Challenge Patterns by Plan Type
### Product Roadmap
- Are we building what customers will pay for, or what they said they wanted?
- Does the velocity estimate account for real team capacity (not theoretical)?
- What happens if the anchor feature takes 3× longer than estimated?
- Who owns decisions when requirements conflict?
### Go-to-Market Plan
- What's the actual ICP conversion rate, not the hoped-for one?
- How many touches to close, and do you have the sales capacity for that?
- What happens if the first 10 deals take 3 months instead of 1?
- Is "land and expand" a real motion or a hope?
### Hiring Plan
- What happens if the key hire takes 4 months to find, not 6 weeks?
- Is the plan dependent on retaining specific people who might leave?
- Does the plan account for ramp time (usually 3–6 months before full productivity)?
- What's the burn impact if headcount leads revenue by 6 months?
### Fundraising Plan
- What's your fallback if the lead investor passes?
- Have you modeled the timeline if it takes 6 months, not 3?
- What's your runway at current burn if the round closes at the low end?
- What assumptions break if you raise 50% of the target amount?
---
## The Hardest Questions
These are the ones people skip:
- "What's the bear case, not the base case?"
- "If this exact plan was run by a team we don't trust, would it work?"
- "What are we not saying out loud because it's uncomfortable?"
- "Who has incentives to make this plan sound better than it is?"
- "What would an enemy of this plan attack first?"
---
## Deliverable
The output of `/em:challenge` is not permission to stop. It's a vulnerability map. Now you can make conscious decisions: validate the risky assumptions, hedge the critical ones, or accept the bets you're making knowingly.
Unknown risks are dangerous. Known risks are manageable.
FILE:executive-mentor/skills/hard-call/SKILL.md
---
name: "hard-call"
description: "/em -hard-call — Framework for Decisions With No Good Options"
---
# /em:hard-call — Framework for Decisions With No Good Options
**Command:** `/em:hard-call <decision>`
For the decisions that keep you up at 3am. Firing a co-founder. Laying off 20% of the team. Killing a product that customers love. Pivoting. Shutting down.
These decisions don't have a right answer. They have a less wrong answer. This framework helps you find it.
---
## Why These Decisions Are Hard
Not because the data is unclear. Often, the data is clear. They're hard because:
1. **Real people are affected** — someone loses a job, a relationship ends, a team is hurt
2. **You've been avoiding the decision** — which means the problem is already worse than it was
3. **Irreversibility** — unlike most business decisions, you can't undo this easily
4. **You have skin in the game** — your judgment about the right call is clouded by your feelings about it
The longer you avoid a hard call, the worse the situation usually gets. The company that needed a 10% cut 6 months ago now needs a 25% cut. The co-founder conversation that should have happened at month 4 is happening at month 14.
**Most hard decisions are late decisions.**
---
## The Framework
### Step 1: The Reversibility Test
The most important question first: **can you undo this?**
- **Reversible** — try it, learn, adjust (fire the vendor, kill the feature, change the strategy)
- **Partially reversible** — painful to undo but possible (restructure, change co-founder roles)
- **Irreversible** — cannot be undone (layoff a person, shut down a product with customer lock-in, close a legal entity)
For irreversible decisions, the bar for certainty is higher. You must do more due diligence before acting. Not because you might be wrong — but because you can't take it back.
**If you're treating a reversible decision like it's irreversible, you're avoiding it.**
### Step 2: The 10/10/10 Framework
Ask three questions about each option:
- **10 minutes from now**: How will you feel immediately after making this decision?
- **10 months from now**: What will the impact be? Will the problem be solved?
- **10 years from now**: When you look back, will this have been the right call?
The 10-minute feeling is usually the least reliable guide. The 10-year view usually clarifies what the right call actually is.
**Most hard decisions look obvious at 10 years. The question is whether you can tolerate the 10-minute pain.**
### Step 3: The Andy Grove Test
Andy Grove's test for strategic decisions: "If we got replaced tomorrow and a new CEO came in, what would they do?"
A fresh set of eyes, no emotional investment in the current path, no sunk cost. What's the obvious right call from the outside?
If the answer is clear to an outsider, the question becomes: why haven't you done it yet?
### Step 4: Stakeholder Impact Mapping
For each option, map who's affected and how:
| Stakeholder | Option A Impact | Option B Impact | Their reaction |
|-------------|----------------|----------------|----------------|
| Affected employees | | | |
| Remaining team | | | |
| Customers | | | |
| Investors | | | |
| You | | | |
This isn't about finding the option that hurts nobody — there isn't one. It's about understanding the full picture before you decide.
### Step 5: The Pre-Announcement Test
Before making the decision: write the announcement. The email to the team, the message to the customer, the conversation you'll have.
**If you can't write that announcement, you're not ready to make the decision.**
Writing it forces you to confront the reality of what you're doing. It also surfaces whether your reasoning holds under examination. "We're making this change because…" — does that sentence ring true?
### Step 6: The Communication Plan
Hard decisions almost always get harder if communication is bad. The decision itself is not the only thing that matters — how it's done matters enormously.
For every hard call, plan:
- **Who needs to know first** (the person directly affected, before anyone else)
- **How you'll tell them** (in person when possible, never via email for personal impact)
- **What you'll say** (honest, direct, compassionate — see `references/hard_things.md`)
- **What they can ask** (be ready for every question)
- **What comes next** (give them a clear picture of what happens after)
---
## Decision-Specific Frameworks
### Firing a Co-Founder
See `references/hard_things.md — Co-Founder Conflicts` for full framework.
Key questions to answer first:
- Is this a performance problem or a values/culture problem? (Different conversations)
- Have you been explicit — not hinted, but direct — about the problem?
- What does the cap table look like and what are the legal implications?
- Is there a role that works better for them, or is this a full exit?
- Who needs to know (board, team, investors) and in what order?
**The rule:** If you've been thinking about this for more than 3 months, you already know the answer. The question is when, not whether.
### Layoffs
Key questions:
- Is this a one-time reset or the beginning of a longer decline? (One reset is recoverable. Serial layoffs kill culture.)
- Are you cutting deep enough? (Insufficient layoffs are worse than no layoffs — two rounds destroys trust.)
- Who owns the announcement and is it direct and honest?
- What's the severance and is it fair?
- How do you prevent the best people from leaving after?
**The rule:** Cut once, cut deep, cut with dignity. Uncertainty is worse than clarity.
### Pivoting
Key questions:
- Is this a true pivot (new direction) or an optimization (same direction, different tactic)?
- What are you keeping and what are you abandoning?
- Do you have evidence the new direction works, or are you running from failure?
- How do you tell current customers who bought the old vision?
- What does this do to the board's confidence?
**The rule:** Pivots should be pulled by evidence of new opportunity, not pushed by failure of the current path.
### Killing a Product Line
Key questions:
- What happens to customers currently using it?
- What's the migration path?
- What do the people who built it do?
- Is "kill it" the right call or is "sell it" or "spin it out" better?
- What's the narrative — internally and externally?
---
## The Avoiding-It Test
You know you've been avoiding a hard call if:
- You've thought about it every week for more than a month
- You're hoping the situation will "resolve itself"
- You're waiting for more data that you'll never feel is enough
- You've had the conversation in your head many times but not in real life
- Other people around you have noticed the problem
**The cost of delay is almost always higher than the cost of the decision.**
Every month you wait, the problem compounds. The co-founder who's not working out becomes more entrenched. The product line that needs to die consumes more resources. The person who needs to be let go affects the people around them.
Make the call. Make it clearly. Make it with dignity.
FILE:executive-mentor/skills/postmortem/SKILL.md
---
name: "postmortem"
description: "/em -postmortem — Honest Analysis of What Went Wrong"
---
# /em:postmortem — Honest Analysis of What Went Wrong
**Command:** `/em:postmortem <event>`
Not blame. Understanding. The failed deal, the missed quarter, the feature that flopped, the hire that didn't work out. What actually happened, why, and what changes as a result.
---
## Why Most Post-Mortems Fail
They become one of two things:
**The blame session** — someone gets scapegoated, defensive walls go up, actual causes don't get examined, and the same problem happens again in a different form.
**The whitewash** — "We learned a lot, we're going to do better, here are 12 vague action items." Nothing changes. Same problem, different quarter.
A real post-mortem is neither. It's a rigorous investigation into a system failure. Not "whose fault was it" but "what conditions made this outcome predictable in hindsight?"
**The purpose:** extract the maximum learning value from a failure so you can prevent recurrence and improve the system.
---
## The Framework
### Step 1: Define the Event Precisely
Before analysis: describe exactly what happened.
- What was the expected outcome?
- What was the actual outcome?
- When was the gap first visible?
- What was the impact (financial, operational, reputational)?
Precision matters. "We missed Q3 revenue" is not precise enough. "We closed $420K in new ARR vs $680K target — a $260K miss driven primarily by three deals that slipped to Q4 and one deal that was lost to a competitor" is precise.
### Step 2: The 5 Whys — Done Properly
The goal: get from **what happened** (the symptom) to **why it happened** (the root cause).
Standard bad 5 Whys:
- Why did we miss revenue? Because deals slipped.
- Why did deals slip? Because the sales cycle was longer than expected.
- Why? Because the customer buying process is complex.
- Why? Because we're selling to enterprise.
- Why? That's just how enterprise sales works.
→ Conclusion: Nothing to do. It's just enterprise.
Real 5 Whys:
- Why did we miss revenue? Three deals slipped out of quarter.
- Why did those deals slip? None of them had identified a champion with budget authority.
- Why did we progress deals without a champion? Our qualification criteria didn't require it.
- Why didn't our qualification criteria require it? When we built the criteria 8 months ago, we were in SMB, not enterprise.
- Why haven't we updated qualification criteria as ICP shifted? No owner, no process for criteria review.
→ Root cause: Qualification criteria outdated, no owner, no review process.
→ Fix: Update criteria, assign owner, add quarterly review.
**The test for a good root cause:** Could you prevent recurrence with a specific, concrete change? If yes, you've found something real.
### Step 3: Distinguish Contributing Factors from Root Cause
Most events have multiple contributing factors. Not all are root causes.
**Contributing factor:** Made it worse, but isn't the core reason. If removed, the outcome might have been different — but the same class of problem would recur.
**Root cause:** The fundamental condition that made the outcome probable. Fix this, and this class of problem doesn't recur.
Example — failed hire:
- Contributing factors: rushed process, reference checks skipped, team under pressure to staff up
- Root cause: No defined competency framework, so interview process varied by who happened to conduct interviews
**The distinction matters.** If you address only contributing factors, you'll have a different-looking but structurally identical failure next time.
### Step 4: Identify the Warning Signs That Were Ignored
Every failure has precursors. In hindsight, they're obvious. The value of this step is making them obvious prospectively.
Ask:
- At what point was the negative outcome predictable?
- What signals were visible at that point?
- Who saw them? What happened when they raised them?
- Why weren't they acted on?
**Common patterns:**
- Signal was raised but dismissed by a senior person
- Signal wasn't raised because nobody felt safe saying it
- Signal was seen but no one had clear ownership to act on it
- Data was available but nobody was looking at it
- The team was too optimistic to take negative signals seriously
This step is particularly important for systemic issues — "we didn't feel safe raising the concern" is a much deeper root cause than "the deal qualification was off."
### Step 5: Distinguish What Was in Control vs. Out of Control
Some failures happen despite correct decisions. Some happen because of incorrect decisions. Knowing the difference prevents both overcorrection and undercorrection.
- **In control:** Process, criteria, team capability, resource allocation, decisions made
- **Out of control:** Market conditions, customer decisions, competitor actions, macro events
For things out of control: what can be done to be more resilient to similar events?
For things in control: what specifically needs to change?
**Warning:** "It was outside our control" is sometimes used to avoid accountability. Be rigorous.
### Step 6: Build the Change Register
Every post-mortem ends with a change register — specific commitments, owned and dated.
**Bad action items:**
- "We'll improve our qualification process"
- "Communication will be better"
- "We'll be more rigorous about forecasting"
**Good action items:**
- "Ravi owns rewriting qualification criteria by March 15 to include champion identification as hard requirement. New criteria reviewed in weekly sales standup starting March 22."
- "By March 10, Elena adds deal-slippage risk flag to CRM for any open opportunity >60 days without a product demo"
- "Maria runs a 30-min retrospective with enterprise sales team every 6 weeks starting April 1, reviews win/loss data"
**For each action:**
- What exactly is changing?
- Who owns it?
- By when?
- How will you verify it worked?
### Step 7: Verification Date
The most commonly skipped step. Post-mortems are useless if nobody checks whether the changes actually happened and actually worked.
Set a verification date: "We'll review whether qualification criteria have been updated and whether deal slippage rate has improved at the June board meeting."
Without this, post-mortems are theater.
---
## Post-Mortem Output Format
```
EVENT: [Name and date]
EXPECTED: [What was supposed to happen]
ACTUAL: [What happened]
IMPACT: [Quantified]
TIMELINE
[Date]: [What happened or was visible]
[Date]: ...
5 WHYS
1. [Why did X happen?] → Because [Y]
2. [Why did Y happen?] → Because [Z]
3. [Why did Z happen?] → Because [A]
4. [Why did A happen?] → Because [B]
5. [Why did B happen?] → Because [ROOT CAUSE]
ROOT CAUSE: [One clear sentence]
CONTRIBUTING FACTORS
• [Factor] — how it contributed
• [Factor] — how it contributed
WARNING SIGNS MISSED
• [Signal visible at what date] — why it wasn't acted on
WHAT WAS IN CONTROL: [List]
WHAT WASN'T: [List]
CHANGE REGISTER
| Action | Owner | Due Date | Verification |
|--------|-------|----------|-------------|
| [Specific change] | [Name] | [Date] | [How to verify] |
VERIFICATION DATE: [Date of check-in]
```
---
## The Tone of Good Post-Mortems
Blame is cheap. Understanding is hard.
The goal isn't to establish that someone made a mistake. The goal is to understand why the system produced that outcome — so the system can be improved.
"The salesperson didn't qualify the deal properly" is blame.
"Our qualification framework hadn't been updated when we moved upmarket, and no one owned keeping it current" is understanding.
The first version fires or shames someone. The second version builds a more resilient organization.
Both might be true simultaneously. The distinction is: which one actually prevents recurrence?
FILE:executive-mentor/skills/stress-test/SKILL.md
---
name: "stress-test"
description: "/em -stress-test — Business Assumption Stress Testing"
---
# /em:stress-test — Business Assumption Stress Testing
**Command:** `/em:stress-test <assumption>`
Take any business assumption and break it before the market does. Revenue projections. Market size. Competitive moat. Hiring velocity. Customer retention.
---
## Why Most Assumptions Are Wrong
Founders are optimists by nature. That's a feature — you need optimism to start something from nothing. But it becomes a liability when assumptions in business models get inflated by the same optimism that got you started.
**The most dangerous assumptions are the ones everyone agrees on.**
When the whole team believes the $50M market is real, when every investor call goes well so you assume the round will close, when your model shows $2M ARR by December and nobody questions it — that's when you're most exposed.
Stress testing isn't pessimism. It's calibration.
---
## The Stress-Test Methodology
### Step 1: Isolate the Assumption
State it explicitly. Not "our market is large" but "the total addressable market for B2B spend management software in German SMEs is €2.3B."
The more specific the assumption, the more testable it is. Vague assumptions are unfalsifiable — and therefore useless.
**Common assumption types:**
- **Market size** — TAM, SAM, SOM; growth rate; customer segments
- **Customer behavior** — willingness to pay, churn, expansion, referrals
- **Revenue model** — conversion rates, deal size, sales cycle, CAC
- **Competitive position** — moat durability, competitor response speed, switching cost
- **Execution** — team velocity, hire timeline, product timeline, operational scaling
- **Macro** — regulatory environment, economic conditions, technology availability
### Step 2: Find the Counter-Evidence
For every assumption, actively search for evidence that it's wrong.
Ask:
- Who has tried this and failed?
- What data contradicts this assumption?
- What does the bear case look like?
- If a smart skeptic was looking at this, what would they point to?
- What's the base rate for assumptions like this?
**Sources of counter-evidence:**
- Comparable companies that failed in adjacent markets
- Customer churn data from similar businesses
- Historical accuracy of similar forecasts
- Industry reports with conflicting data
- What competitors who tried this found
The goal isn't to find a reason to stop — it's to surface what you don't know.
### Step 3: Model the Downside
Most plans model the base case and the upside. Stress testing means modeling the downside explicitly.
**For quantitative assumptions (revenue, growth, conversion):**
| Scenario | Assumption Value | Probability | Impact |
|----------|-----------------|-------------|--------|
| Base case | [Original value] | ? | |
| Bear case | -30% | ? | |
| Stress case | -50% | ? | |
| Catastrophic | -80% | ? | |
Key question at each level: **Does the business survive? Does the plan make sense?**
**For qualitative assumptions (moat, product-market fit, team capability):**
- What's the earliest signal this assumption is wrong?
- How long would it take you to notice?
- What happens between when it breaks and when you detect it?
### Step 4: Calculate Sensitivity
Some assumptions matter more than others. Sensitivity analysis answers: **if this one assumption changes, how much does the outcome change?**
Example:
- If CAC doubles, how does that change runway?
- If churn goes from 5% to 10%, how does that change NRR in 24 months?
- If the deal cycle is 6 months instead of 3, how does that affect Q3 revenue?
High sensitivity = the assumption is a key lever. Wrong = big problem.
### Step 5: Propose the Hedge
For every high-risk assumption, there should be a hedge:
- **Validation hedge** — test it before betting on it (pilot, customer conversation, small experiment)
- **Contingency hedge** — if it's wrong, what's plan B?
- **Early warning hedge** — what's the leading indicator that would tell you it's breaking before it's too late to act?
---
## Stress Test Patterns by Assumption Type
### Revenue Projections
**Common failures:**
- Bottom-up model assumes 100% of pipeline converts
- Doesn't account for deal slippage, churn, seasonality
- New channel assumed to work before tested at scale
**Stress questions:**
- What's your actual historical win rate on pipeline?
- If your top 3 deals slip to next quarter, what happens to the number?
- What's the model look like if your new sales rep takes 4 months to ramp, not 2?
- If expansion revenue doesn't materialize, what's the growth rate?
**Test:** Build the revenue model from historical win rates, not hoped-for ones.
### Market Size
**Common failures:**
- TAM calculated top-down from industry reports without bottoms-up validation
- Conflating total market with serviceable market
- Assuming 100% of SAM is reachable
**Stress questions:**
- How many companies in your ICP actually exist and can you name them?
- What's your serviceable obtainable market in year 1-3?
- What percentage of your ICP is currently spending on any solution to this problem?
- What does "winning" look like and what market share does that require?
**Test:** Build a list of target accounts. Count them. Multiply by ACV. That's your SAM.
### Competitive Moat
**Common failures:**
- Moat is technology advantage that can be built in 6 months
- Network effects that haven't yet materialized
- Data advantage that requires scale you don't have
**Stress questions:**
- If a well-funded competitor copied your best feature in 90 days, what do customers do?
- What's your retention rate among customers who have tried alternatives?
- Is the moat real today or theoretical at scale?
- What would it cost a competitor to reach feature parity?
**Test:** Ask churned customers why they left and whether a competitor could have kept them.
### Hiring Plan
**Common failures:**
- Time-to-hire assumes standard recruiting cycle, not current market
- Ramp time not modeled (3-6 months before full productivity)
- Key hire dependency: plan only works if specific person is hired
**Stress questions:**
- What happens if the VP Sales hire takes 5 months, not 2?
- What does execution look like if you only hire 70% of planned headcount?
- Which single person, if they left tomorrow, would most damage the plan?
- Is the plan achievable with current team if hiring freezes?
**Test:** Model the plan with 0 net new hires. What still works?
### Competitive Response
**Common failures:**
- Assumes incumbents won't respond (they will if you're winning)
- Underestimates speed of response
- Doesn't model resource asymmetry
**Stress questions:**
- If the market leader copies your product in 6 months, how does pricing change?
- What's your response if a competitor raises $30M to attack your space?
- Which of your customers have vendor relationships with your competitors?
---
## The Stress Test Output
```
ASSUMPTION: [Exact statement]
SOURCE: [Where this came from — model, investor pitch, team gut feel]
COUNTER-EVIDENCE
• [Specific evidence that challenges this assumption]
• [Comparable failure case]
• [Data point that contradicts the assumption]
DOWNSIDE MODEL
• Bear case (-30%): [Impact on plan]
• Stress case (-50%): [Impact on plan]
• Catastrophic (-80%): [Impact on plan — does the business survive?]
SENSITIVITY
This assumption has [HIGH / MEDIUM / LOW] sensitivity.
A 10% change → [X] change in outcome.
HEDGE
• Validation: [How to test this before betting on it]
• Contingency: [Plan B if it's wrong]
• Early warning: [Leading indicator to watch — and at what threshold to act]
```
FILE:founder-coach/SKILL.md
---
name: "founder-coach"
description: "Personal leadership development for founders and first-time CEOs. Covers founder archetype identification, delegation frameworks, energy management, CEO calendar audits, leadership style evolution, blind spot identification, imposter syndrome, founder mental health, and succession planning. Use when a founder feels like the bottleneck, struggles to delegate, is burning out, transitioning from IC to executive, managing a board, or when user mentions founder mode, CEO growth, leadership development, delegation, burnout, or imposter syndrome."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: founder-development
updated: 2026-03-05
frameworks: leadership-growth, founder-toolkit
---
# Founder Development Coach
Your company can only grow as fast as you do. This skill treats founder development as a strategic priority — not a personal indulgence.
## Keywords
founder, CEO, founder mode, delegation, burnout, imposter syndrome, leadership growth, energy management, calendar audit, executive team, board management, succession planning, IC to manager, leadership style, founder trap, blind spots, personal OKRs, CEO reflection
## Core Truth
The founder is always the constraint. Not intentionally — it's structural. You built the company. You know everything. Decisions flow through you. This works until it doesn't.
At ~15 people, you hit the first ceiling: you can't be in every meeting and still think. At ~50 people, the second: your style starts creating culture problems. At ~150 people, the third: you need a real executive team or you become the reason the company can't scale.
The earlier you address this, the better.
---
## 1. Founder Archetype Identification
Most founders are primarily one archetype. Knowing yours predicts what you'll struggle with.
| Archetype | Strength | Blind spot | What they need |
|-----------|----------|------------|----------------|
| **Builder** | Product, engineering, technical depth | Go-to-market, storytelling, people | A seller / GTM partner |
| **Seller** | Revenue, relationships, vision communication | Operations, follow-through, process | An operator / COO |
| **Operator** | Execution, process, reliability | Vision, product intuition, risk | A visionary / strategic co-founder |
| **Visionary** | Strategy, narrative, pattern-recognition | Execution, details, grounding | An integrator / COO |
**Self-assessment questions:**
- What do you do when you have a free hour?
- What do you procrastinate on most?
- What do your co-founders or early team complain you don't do?
- What's the best feedback you've received about your leadership?
Most founders are Builder or Visionary. Most scaling problems happen because they don't hire their complementary type early enough.
---
## 2. Delegation Framework
Founders fail to delegate for four reasons:
1. "Nobody does it as well as I do" (often true short-term, fatal long-term)
2. "It takes longer to explain than to do it" (true once; not true the 10th time)
3. "I lose control if I don't do it myself" (control is an illusion at scale)
4. "If it fails, it's my fault" (it's your fault if you never let anyone else try)
### The Skill × Will Matrix
| | High Skill | Low Skill |
|---|-----------|----------|
| **High Will** | Delegate fully | Coach and develop |
| **Low Will** | Motivate or reassign | Manage out or redesign role |
**Rules:**
- High skill + high will → Give the work and get out of the way
- High will + low skill → Invest in them. They want to grow.
- High skill + low will → Find out why. Fix the environment or accept the mismatch.
- Low skill + low will → Don't delegate to them. Address the performance issue.
### The Delegation Ladder
Not all delegation is equal. Build up gradually:
1. "Do exactly what I tell you" — not delegation, instruction
2. "Research this and report back" — information gathering
3. "Propose a solution and I'll decide" — thinking delegation
4. "Decide and tell me what you decided" — decision delegation with review
5. "Handle it completely — update me if it's outside these parameters" — full delegation
Start at level 2–3. Move people up as trust is established. Most founders never get past level 3 with their team — that's the bottleneck.
### What to delegate first
**Delegate first (high volume, low stakes):**
- Recurring operational tasks you do the same way every time
- Information gathering and synthesis
- Meeting coordination and scheduling
- Reports and updates you produce regularly
**Delegate next (skill-buildable):**
- Customer interactions (with clear principles)
- Hiring screens (after you've trained judgment)
- Partner relationship management
- Budget management within parameters
**Delegate last (strategic, irreversible):**
- Major strategic pivots
- Executive hires
- Large financial commitments
- M&A decisions
---
## 3. Energy Management
Founders manage energy, not just time. Time is fixed. Energy is renewable — but only if you manage it.
### The Energy Audit
Map your week by energy, not tasks. See `references/founder-toolkit.md` for the full template.
**Categories:**
- 🟢 **Energizing:** Activities that leave you sharper after doing them
- 🟡 **Neutral:** Neither energizing nor draining
- 🔴 **Draining:** Activities that leave you depleted
**Common founder energy patterns:**
- **Builders:** Energized by creating, drained by politics and process
- **Sellers:** Energized by people and wins, drained by detail work and admin
- **Operators:** Energized by solving, drained by ambiguity and indecision
- **Visionaries:** Energized by strategy and ideas, drained by execution and repetition
**The rule:** Maximize green. Eliminate or delegate red. Accept yellow as the price of leadership.
### Energy management practices
**Protect deep work time.** 2–4 hours of uninterrupted thinking time, 3–5 days per week. Schedule it. Defend it. This is where strategy happens.
**Batch shallow work.** Email, Slack, administrative tasks — twice a day maximum.
**Single-task during recovery.** If you're depleted, don't try to do your best work. Do tasks that don't require your best.
**Identify your peak window.** Most people have 4–6 peak hours per day. Schedule your hardest work in those windows.
---
## 4. CEO Calendar Audit
The calendar is the most honest document in a founder's life. It shows what you actually prioritize, not what you say you prioritize.
### Running the audit
Pull the last 4 weeks of calendar data. Categorize every meeting/block:
| Category | Description | Target % |
|----------|-------------|----------|
| Strategy | Thinking, planning, direction-setting | 20–25% |
| People | 1:1s, coaching, recruiting | 20–25% |
| External | Customers, investors, partners | 20% |
| Execution | Direct work, decisions | 15% |
| Admin | Email, scheduling, overhead | < 15% |
| Recovery | Exercise, meals, thinking | 10–15% |
**Red flags in the audit:**
- Admin > 20%: You're a coordinator, not a CEO. Fix your systems.
- Execution > 30%: You're still an IC. Build the team.
- People < 10%: Your team is running on empty. They need more of you.
- No recovery blocks: You're running on adrenaline. It ends badly.
- Strategy < 10%: You're running the company, not leading it.
### The CEO's primary job at each stage
| Stage | CEO should spend most time on... |
|-------|--------------------------------|
| Seed | Product and customers. Directly. |
| Series A | Hiring the executive team. Recruiting is your job. |
| Series B | Culture, strategy, and external (investors/partners/customers) |
| Series C+ | Vision, board, external narrative, executive development |
If you're spending time on things from two stages ago, you haven't made the transition.
---
## 5. Leadership Style Evolution
The job changes at every stage. Most founders don't change with it.
**IC → Manager (0 to ~10 people):**
You need to teach and build trust. People are watching how you treat failure. The skill: give clear context, set expectations, check in frequently.
**Manager → Leader (~10 to ~50 people):**
You can't manage everyone directly. You need people who manage people. The skill: hire managers you trust, let them manage.
**Leader → Executive (~50 to ~200 people):**
You're now setting culture and direction, not managing work. The skill: communicate obsessively, decide at the right altitude, develop your leadership team.
**Executive → Institutional CEO (200+):**
You're a symbol as much as a manager. The skill: build systems that work without you; focus on board, investors, and external narrative.
**The hardest transition:** Manager → Leader. You have to stop doing things yourself and trust people you're still getting to know.
---
## 6. Blind Spot Identification
Everyone has them. Founders more than most — because nobody in the early company had the authority or safety to tell you.
### Common founder blind spots
- **Communication:** "I said it once, they should know" — you said it; they didn't hear it or didn't believe it
- **Decision speed:** Moving so fast that teams can't orient or build on your direction
- **Context hoarding:** Knowing what's happening without sharing it, then being frustrated that teams make bad decisions
- **Optimism bias:** Consistently underestimating timelines, cost, and difficulty
- **Founder exceptionalism:** Rules that apply to everyone don't apply to you
- **Feedback avoidance:** Creating an environment where no one gives you honest feedback
### How to find your blind spots
1. **360 feedback (anonymous):** Once a year. Ask direct reports, peers, board members. Include "What does [name] do that gets in the way of our success?"
2. **Exit interview analysis:** What do departing employees consistently say? Find the pattern.
3. **Failure post-mortems:** What do your worst decisions have in common? What were you assuming that wasn't true?
4. **The energy audit:** Where do you consistently drain the people around you?
---
## 7. Imposter Syndrome Toolkit
It doesn't go away. It evolves. The founder who was scared to pitch to investors is now scared to manage a board. The founder who was scared to hire is now scared to fire.
**The reframe:** Imposter syndrome is proportional to stretch. If you never feel it, you're not growing.
**Practical tools:**
- **Evidence file:** Document wins, compliments, decisions that worked. Read it when the doubt hits.
- **Normalize the feeling:** "I feel underprepared for this" ≠ "I am an imposter." Feeling and fact are different.
- **Do the thing anyway.** Competence comes from doing, not from feeling ready.
- **Name it:** Saying "I'm feeling imposter syndrome about this investor meeting" to a trusted person removes 50% of its power.
---
## 8. Founder Mental Health
Burnout isn't weakness. It's a predictable outcome of high-demand + low-recovery + no control over inputs.
### Burnout signals
Early: Irritability, difficulty sleeping, decisions feel harder than they should, loss of enthusiasm for the mission.
Mid: Physical symptoms (headaches, illness), cynicism about the company, social withdrawal, all tasks feel equally important (priority paralysis).
Late: Can't function, decisions have stopped, team notices before you do.
**If you're in late burnout:** Stop performing. Get support. The company needs a functioning founder more than it needs a martyred one.
### Structural prevention
- **Protect recovery time.** Not weekends — protected time during the week where you're not available.
- **Therapy or coaching.** Not optional for founders. The job is isolating and the stakes are high.
- **Peer group.** Other founders at similar stages. They're the only people who actually understand the job.
- **Clear off-ramps.** Know what "enough for today" looks like. Don't let the work be infinite.
---
## 9. The Founder Mode Trap
Paul Graham's "Founder Mode" essay made the case that great founders stay deeply involved in operations — skip middle management and go direct. It resonated because it's sometimes true.
**When founder mode helps:**
- Crisis recovery (company needs direct leadership)
- Product-market fit search (speed matters more than org health)
- High-value, irreversible decisions (you should be in the room)
- Early stages when the team is small
**When founder mode hurts:**
- When it undermines managers you've hired (they can't lead if you override them)
- When it's driven by distrust rather than strategy
- When it prevents the team from developing judgment
- When you're doing it because you miss doing, not because the company needs you to
**The test:** Are you going deep because the situation requires it, or because you're uncomfortable with the loss of control? The first is leadership. The second is the trap.
---
## 10. Succession Planning
Building a company that works without you is not disloyalty — it's the ultimate expression of leadership.
**Succession is not just about exit.** It's about resilience. What happens if you're sick? On sabbatical? Acquired?
**Succession readiness levels:**
- Level 1: You've documented your key knowledge and processes
- Level 2: At least one person can cover each of your key functions for 2 weeks
- Level 3: Your leadership team can run the company for a quarter without you
- Level 4: You've identified and developed your potential successor
Most founders are at Level 0. Level 2 is a reasonable target. Level 3 is a strategic asset.
---
## Key Questions for Founder Development
- "What decisions did you make last week that someone else could have made?"
- "What are you still doing that you should have delegated 6 months ago?"
- "When did you last get honest, critical feedback? From whom? What did it say?"
- "What would need to be true for the company to run for a week without you?"
- "What's draining your energy that you've accepted as unavoidable?"
## Detailed References
- `references/leadership-growth.md` — Maxwell levels, situational leadership, founder-to-CEO transition
- `references/founder-toolkit.md` — Weekly reflection, energy audit, delegation matrix, 1:1 templates
FILE:founder-coach/references/founder-toolkit.md
# Founder Toolkit
Practical tools for founder self-management and leadership development.
---
## 1. Weekly CEO Reflection Template
**15 minutes. Every Friday. No excuses.**
This is the most important meeting of the week. You with yourself.
```
DATE: _______________
## This Week
**1. What was my most important contribution this week?**
(Not the longest meeting or the hardest problem — the thing that will matter in 90 days.)
_______________________________________________
**2. Where did I add the least value? Why was I involved?**
(Be honest. Where were you in the room out of habit, not necessity?)
_______________________________________________
**3. What should I have delegated but didn't?**
(Name the specific task and the person you could have delegated it to.)
_______________________________________________
**4. What decision am I avoiding? Why?**
(Fear of being wrong? Not enough information? Conflict avoidance?)
_______________________________________________
**5. What would I do differently this week if I could do it over?**
(One thing. Make it specific.)
_______________________________________________
## Next Week
**My one most important outcome for next week:**
_______________________________________________
**What will I stop doing / not start / protect myself from?**
_______________________________________________
```
---
## 2. Energy Audit Template
Map your week by energy, not tasks. Do this for one full work week.
### Step 1: Time block mapping
For each 30-minute block in your week, record:
- What you did
- Energy level: 🟢 Energizing / 🟡 Neutral / 🔴 Draining
```
Monday:
08:00-08:30: __________________ [🟢/🟡/🔴]
08:30-09:00: __________________ [🟢/🟡/🔴]
09:00-09:30: __________________ [🟢/🟡/🔴]
... (continue through the day)
```
### Step 2: Pattern analysis
After one week, categorize activities:
| Activity type | Energy level | Total hours | % of week |
|--------------|-------------|-------------|-----------|
| Customer calls | | | |
| Investor meetings | | | |
| Team 1:1s | | | |
| Product decisions | | | |
| Strategy/planning | | | |
| Email/Slack | | | |
| Recruiting | | | |
| Financial review | | | |
| External talks/events | | | |
| Administrative tasks | | | |
| Deep work/building | | | |
| Recovery/breaks | | | |
### Step 3: Optimization plan
**Green activities to protect (min 40% of week):**
- _______________________________________________
**Red activities to eliminate or delegate (target: < 15% of week):**
- Activity: __________________ → Delegate to: __________________
- Activity: __________________ → Eliminate via: __________________
**Your personal energy peak hours:**
I do my best thinking: _______ to _______
Schedule this time as: Protected deep work (no meetings)
---
## 3. Delegation Matrix
For every task you regularly do, run it through this matrix.
### Assessment
| Task | Skill level needed | My will to keep it | Decision |
|------|-------------------|-------------------|----------|
| | High / Med / Low | High / Med / Low | Keep / Coach / Delegate / Kill |
### Delegation scoring
| My Skill | My Will | Decision |
|----------|---------|----------|
| High | High | Keep — this is your zone of genius |
| High | Low | Delegate — you can do it, but it drains you. Train someone. |
| Low | High | Develop — learn it or hire for it |
| Low | Low | Kill or outsource — why is this on your plate? |
### The 70% rule
If someone can do a task 70% as well as you, delegate it. Trying to get to 100% is a trap:
- Their 70% will grow to 90% with practice
- Your 30% extra effort costs more than the quality gap
- You free up time for things only you can do
---
## 4. 1:1 Template for Direct Reports
Weekly or biweekly. 30 minutes. Their agenda, not yours.
```
DATE: _______________
PERSON: _______________
## Their Section (first 20 min)
**What's on their mind? (open the meeting with this)**
(No agenda from you first — let them lead)
**What are they working on? Where are they stuck?**
**What do they need from me?**
**Anything they wanted to raise but haven't had the chance to?**
## Your Section (last 10 min)
**Context to share (strategy, changes, what they should know):**
**Direct feedback to give (if any):**
- Be specific: "In Tuesday's meeting, when you [did X], the impact was [Y]"
- Make it actionable: "Next time, I'd suggest [Z]"
**Career/growth check-in (monthly, not every meeting):**
- How are they feeling about their growth?
- What do they want to be doing more of?
- What are they interested in that they're not currently doing?
## Follow-ups
| Commitment | Owner | Due |
|------------|-------|-----|
| | | |
```
### Rules for effective 1:1s
- **Their agenda first.** If you dominate with your updates, they stop bringing theirs.
- **No status updates.** That's what tools are for. This time is for their thinking, blockers, and development.
- **Consistent time.** Rescheduled 1:1s signal that they're not a priority.
- **Take notes.** Review them before the next meeting. It signals that you listened.
- **Follow up on commitments.** If you say "I'll get you that answer by Thursday," get it by Thursday.
---
## 5. Personal OKRs for the Founder
Most founders hold their team accountable to goals but have none themselves. Fix that.
### Template: Quarterly Personal OKRs
```
Q[X] YYYY | FOUNDER OKRs
## My One Priority This Quarter
(The single most important thing I personally must accomplish)
_______________________________________________
## Objective 1: [Leadership Development]
What I'm trying to achieve: _______________________________________________
KR 1.1: [Measurable outcome by EoQ]
KR 1.2: [Measurable outcome by EoQ]
KR 1.3: [Measurable outcome by EoQ]
Progress check (mid-quarter): _______________________________________________
## Objective 2: [Delegation / Team Building]
What I'm trying to achieve: _______________________________________________
KR 2.1: [Measurable outcome by EoQ]
KR 2.2: [Measurable outcome by EoQ]
## Objective 3: [External Impact — Investors / Customers / Market]
What I'm trying to achieve: _______________________________________________
KR 3.1: [Measurable outcome by EoQ]
KR 3.2: [Measurable outcome by EoQ]
## The "Stop Doing" List (equally important)
Things I'm committing to stop doing this quarter:
- Stop: _______________________________________________
- Stop: _______________________________________________
- Stop: _______________________________________________
```
### Personal OKR examples
**Objective: Become a better coach, not just a decision-maker**
- KR: 90% of my direct reports can make their top 3 recurring decisions without me by EoQ
- KR: In 1:1 reviews, 80% of team rates me as "helps me think through problems" vs "tells me what to do"
- KR: Conduct quarterly 360 feedback session with all direct reports
**Objective: Build investor trust before I need it**
- KR: Monthly investor updates sent within 5 days of month-end, every month this quarter
- KR: 1:1 calls with each board member, once per quarter, outside of board meetings
- KR: Create and share 3-year financial model with board by EoQ
**Objective: Protect my energy and performance**
- KR: 3+ hours of protected deep work time per day, 4+ days per week
- KR: Complete weekly CEO reflection every Friday (track: 0/13 weeks → 13/13)
- KR: Zero email after 8pm, zero weekends unless explicit crisis
---
## 6. The "Stop Doing" List
The hardest list to make and the most valuable to keep.
Most founders have clear to-do lists. Few have stop-doing lists. The asymmetry is the problem.
### The stop-doing audit
**Things to stop doing immediately (decision you can make today):**
- Attending meetings you don't add value to
- Being the default person for decisions that should be made by others
- Redoing work that your team completed
- Checking email/Slack during deep work blocks
- Starting tasks you know you'll delegate partway through
**Things to stop doing by delegating (need to train someone):**
- _______________________________________________
- _______________________________________________
- _______________________________________________
**Things to stop doing by building systems:**
- Recurring manual tasks → automate
- Recurring decisions → write decision criteria so others can decide
- Recurring explanations → document once, reference always
### The decision filter
Before accepting new responsibilities, run through:
1. Does this require something only I can do?
2. Is this the highest and best use of my time?
3. If I say yes to this, what am I saying no to?
If the answers are no, no, and something important — say no.
---
## 7. Evidence File
For when imposter syndrome hits. Keep a running file of:
**Wins** (monthly minimum)
- Company milestones you led
- Decisions that worked out well
- Feedback you received that was genuinely positive
**Quotes** (capture as they happen)
- Direct quotes from team members, customers, investors about your impact
- Emails or messages that reflect trust or appreciation
**The hard calls that paid off**
- Decisions you were scared to make that turned out well
- Times you said no to something that would have hurt the company
**When to read it:** When you're doubting yourself before a board meeting, a hard conversation, a big pitch. The feeling isn't fact. The evidence file is.
FILE:founder-coach/references/leadership-growth.md
# Leadership Growth Reference
Frameworks for founder and executive leadership development.
---
## 1. The 5 Levels of Leadership (Maxwell)
John Maxwell's model describes leadership development as a ladder. Most founders start at Level 2–3 and need to reach Level 4–5 to scale effectively.
| Level | Name | People follow because... | What it looks like |
|-------|------|--------------------------|-------------------|
| 1 | Position | They have to (title/authority) | "Do this because I'm the CEO" |
| 2 | Permission | They want to (relationship) | People choose to work with you beyond the job requirement |
| 3 | Production | You produce results | Team rallies because you deliver; your track record gives credibility |
| 4 | People Development | You develop others | You're multiplying leaders; your success is measured by others' growth |
| 5 | Pinnacle | Who you are (reputation) | People follow because of what you've built and who you've become |
**Most founders are at Level 3.** They got here by building and shipping. The path to scaling is Level 4: developing other leaders.
**The Level 3 trap:** Production-focused founders attract doers, not leaders. They value results over growth. Their teams are effective but dependent. Every decision still goes through the founder.
**The Level 4 shift:** Measure your success by how well your team succeeds without you. Your job is to make the people around you better.
---
## 2. Situational Leadership Model
Ken Blanchard's model says effective leadership style shifts based on the person and the task — not the leader's preference.
Four styles based on the follower's development level:
| Development Level | Competence | Commitment | Leadership Style | What to do |
|------------------|------------|------------|-----------------|------------|
| D1 — Enthusiastic Beginner | Low | High | S1: Directing | High direction, low support. Tell them what to do. |
| D2 — Disillusioned Learner | Low/Med | Low | S2: Coaching | High direction + high support. Teach and encourage. |
| D3 — Capable but Cautious | Medium/High | Variable | S3: Supporting | Low direction, high support. Collaborate and encourage. |
| D4 — Self-Reliant Achiever | High | High | S4: Delegating | Low direction, low support. Get out of the way. |
**Common founder error:** Using the same leadership style with everyone. The founder who directs a D4 will frustrate them into leaving. The founder who delegates to a D1 will watch them fail.
**Diagnosis before deciding:**
Before determining your style, ask for each person + task:
- How much do they know about this specific task? (Not in general — this task.)
- How much do they want to do this specific task?
These answers may surprise you. A senior engineer may be D4 on architecture and D1 on customer calls.
---
## 3. The Founder → CEO Transition
The hardest leadership change most founders face, and nobody prepares them for it.
### What changes
**As a founder, you were judged on:**
- What you personally built
- How fast you moved
- Your own output
**As a CEO, you're judged on:**
- What your team produced
- How effectively you set direction
- The quality of the people around you
The skills that made you a great founder — doing, deciding, building — can actively work against you as a CEO.
### The transition phases
**Phase 1: Still doing (0–15 people)**
You're right to be deep in the work. Speed requires it. Your personal output matters.
Risk: Staying here too long.
**Phase 2: Building around you (15–50 people)**
You're hiring and starting to delegate. People do work you used to do.
Challenge: Learning to trust output that doesn't look like yours.
Failure mode: Hiring people and then redoing their work.
**Phase 3: Leading through leaders (50–150 people)**
You no longer know everything happening in the company. That's correct.
Challenge: Managing people who manage people — twice removed from the work.
Failure mode: Bypassing your managers to go direct (undermines them, creates chaos).
**Phase 4: Setting the container (150+ people)**
Your job is culture, strategy, and the senior leadership team. You're a CEO, not a senior contributor.
Challenge: Staying relevant and strategic without getting lost in the weeds.
Failure mode: Retreating to execution to feel productive.
### The emotional reality
Most founders describe the transition as:
- A loss of identity ("I used to know everything that was happening")
- A loss of control ("Decisions happen without me")
- A loss of clarity ("Was I more effective before?")
These are real losses, not just discomfort. Acknowledge them. Find identity in what the CEO role is, not what the founder role was.
---
## 4. Building Your Executive Team
### When to hire your first executive
Common question: "When do I need a VP/C-suite?"
**Trigger signs:**
- The function is failing and you can't fix it by working harder
- You can't attract or develop talent in that function because you lack the expertise
- The function is growing faster than you can lead it
- You're making bad decisions in that domain because you don't have deep knowledge
**Order of first executives:**
Most companies hire in this order, but the right order depends on your archetype and what's breaking:
1. First non-founder exec is usually Sales (VP Sales) or Engineering (VP Eng / CTO)
2. Then COO/Operations when coordination becomes the bottleneck
3. Then Finance (CFO) when fundraising or financial complexity demands it
4. Then People/HR when hiring velocity and culture require dedicated ownership
### How to onboard executives
**The 30-60-90 plan:**
- Day 1–30: Listen. Meet everyone. Learn the current state. No major decisions.
- Day 31–60: Diagnose. What's working, what isn't, what's missing. Share findings.
- Day 61–90: Act. Make changes. Start building systems. Establish their leadership presence.
**The trust-building sequence:**
Start with small, visible wins. Let them prove themselves in low-stakes situations before handing over high-stakes decisions.
**The founder's role during exec onboarding:**
- Provide context generously
- Introduce them with genuine authority ("This is the decision-maker for X — go to them, not me")
- Don't override their decisions publicly
- Give feedback privately, not in front of their team
**Failure mode:** Hiring a great executive and then making them feel like a senior employee. If you override every major decision, you don't have an executive — you have an expensive advisor.
---
## 5. Managing Your Board
### The fundamental tension
You work for the board. The board elected you. They can remove you. This is a governance reality, not a threat.
And: You lead the company. The board sets governance and approves major decisions, but they're not running the business day-to-day. You are.
**Healthy dynamic:** Board holds accountability; CEO holds authority. They're not adversarial — they're complementary.
### The founder mistake
Most founders either:
1. **Over-inform:** Share every detail, create noise, invite micro-management
2. **Under-inform:** Share only wins, board is surprised by problems, trust erodes
Neither works. The goal is strategic partnership.
### What the board actually needs
- **Monthly written update:** Financial performance vs plan, key metrics, top 3 issues + proposed solutions, forward-looking risks. 1–2 pages.
- **Quarterly board meeting:** Strategic discussion, not financial recap. They've read the update. Use the time for decisions and input.
- **Real-time alerts:** Big bad news before the meeting. Never let board members be surprised by negative news they should have known earlier.
### Managing board members individually
Invest in 1:1 relationships with each board member between meetings. Understand what they care about. Use their expertise.
Board members who feel informed and useful are your allies. Board members who feel blindsided or sidelined become difficult.
**The pre-meeting call:** Before every board meeting, call each member individually. Preview the agenda, surface concerns, align on decisions. The meeting itself should have no surprises.
### When the board challenges you
"The board doesn't trust my judgment" is often really: "I haven't given them enough information to trust my judgment."
Fix the transparency gap before assuming it's a political problem.
**When the board is actually wrong:** Make the case clearly, once, with data. If they override you on something important and you can't accept it, that's a signal about fit. Founders get removed. It happens. Build board relationships before you need them to trust you on a hard call.
FILE:internal-narrative/SKILL.md
---
name: "internal-narrative"
description: "Build and maintain one coherent company story across all audiences — employees, investors, customers, candidates, and partners. Detects narrative contradictions and ensures the same truth is framed for each audience's needs. Use when preparing investor updates, all-hands presentations, board communications, recruiting narratives, crisis communications, or when user mentions company narrative, messaging consistency, storytelling, all-hands, investor update, or crisis communication."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: narrative-strategy
updated: 2026-03-05
frameworks: narrative-frameworks, all-hands-template
---
# Internal Narrative Builder
One company. Many audiences. Same truth — different lenses. Narrative inconsistency is trust erosion. This skill builds and maintains coherent communication across every stakeholder group.
## Keywords
narrative, company story, internal communication, investor update, all-hands, board communication, crisis communication, messaging, storytelling, narrative consistency, audience translation, founder narrative, employee communication, candidate narrative, partner communication
## Core Principle
**The same fact lands differently depending on who hears it and what they need.**
"We're shifting resources from Product A to Product B" means:
- To employees: "Is my job safe? Why are we abandoning what I built?"
- To investors: "Smart capital allocation — they're doubling down on the winner"
- To customers of Product A: "Are they abandoning us?"
- To candidates: "Exciting new focus — are they decisive?"
Same fact. Four different narratives needed. The skill is maintaining truth while serving each audience's actual question.
---
## Framework
### Step 1: Build the Core Narrative
One paragraph that every other communication derives from. This is the source of truth.
**Core narrative template:**
> [Company name] exists to [mission — present tense, specific]. We're building [what you're building] because [the problem you're solving]. Our approach is [your unique way of doing this]. We're at [honest description of current state] and heading toward [where you're going in concrete terms].
**Good core narrative (example):**
> Acme Health exists to reduce preventable falls in elderly care using smartphone-based mobility analysis. We're building an AI diagnostic tool for care teams because current fall risk assessments are subjective, infrequent, and often wrong. Our approach — using the phone's camera during a 10-second walking test — means no new hardware, no specialist required. We have 80 care facilities in DACH paying us €800K ARR, and we're heading to €3M ARR by demonstrating clinical value at scale before our Series B.
**Bad core narrative:**
> Acme Health is an innovative AI company revolutionizing elderly care through cutting-edge technology that empowers care providers and improves patient outcomes across the continuum of care.
The good version is usable. The bad version says nothing.
---
### Step 2: Audience Translation Matrix
Take the core narrative and translate it for each audience. Same truth, different frame.
| Fact | Employees need to hear | Investors need to hear | Customers need to hear | Candidates need to hear |
|------|----------------------|----------------------|----------------------|------------------------|
| We have 80 customers | "We've proven the model — your work matters" | "Product-market fit signal, capital efficient" | "80 care facilities trust us" | "Traction you'd be joining" |
| We pivoted from hardware | "We were honest enough to change course" | "Capital-efficient pivot to better unit economics" | "We found a faster, simpler way to serve you" | "We make decisions based on evidence, not ego" |
| We missed Q2 revenue | "Here's why, here's the plan, here's what you can do" | "Revenue mix shifted — trailing indicator improving" | [Usually don't tell customers revenue misses] | [Usually not shared externally] |
| We're hiring fast | "The team is growing — your network matters" | "Headcount plan aligned to growth" | [Not relevant unless it affects service] | "This is a rocket ship moment" |
**Rules:**
- Never contradict yourself across audiences. Different framing ≠ different facts.
- "We told investors growth, told employees efficiency" is a contradiction. Audit for this.
- Investors and employees see each other. Board members talk to your team. Candidates google you.
---
### Step 3: Contradiction Detection
Before any major communication, run the contradiction check:
**Question 1:** What did we tell investors last month about [topic]?
**Question 2:** What did we tell employees about the same topic?
**Question 3:** Are these consistent? If not — which version is true?
**Common contradictions:**
- "Efficient growth" to investors + "we're hiring aggressively" to candidates
- "Strong pipeline" to investors + "sales is struggling" at all-hands
- "Customer-first" in culture + recent decisions that clearly prioritized revenue over customer need
**When you catch a contradiction:** Fix the less accurate version, then communicate the correction explicitly. "Last month I said X. After more reflection, X is not quite right. Here's the clearer version."
Correcting yourself before someone else catches it builds more trust than getting caught.
---
### Step 4: Audience-Specific Communication Cadence
| Audience | Format | Frequency | Owner |
|----------|--------|-----------|-------|
| Employees | All-hands | Monthly | CEO |
| Employees | Team updates | Weekly | Team leads |
| Investors | Written update | Monthly | CEO + CFO |
| Board | Board meeting + memo | Quarterly | CEO |
| Customers | Product updates | Per release | CPO / CS |
| Candidates | Careers page + interview narrative | Ongoing | CHRO + Founders |
| Partners | Quarterly business review | Quarterly | BD Lead |
---
### Step 5: All-Hands Structure and Cadence
See `templates/all-hands-template.md` for the full template.
**Principles:**
- Lead with honest state of the company. No spin.
- Connect company performance to individual work: "Here's how what you built contributed to this outcome."
- Give people a reason to be proud of their choice to work here.
- Leave time for real Q&A — not curated questions.
**All-hands failure modes:**
- CEO speaks for 55 of 60 minutes; Q&A is "any quick questions?"
- All good news, all the time — employees know when you're not being honest
- Metrics without context: "ARR grew 15%" without explaining if that's good, bad, or expected
- Questions deflected: "That's a great point, we should follow up on that" → never followed up
---
### Step 6: Crisis Communication
When the narrative breaks — someone leaves publicly, a product fails, a security breach, a press article.
**The 4-hour rule:** If something is public or about to be, communicate internally within 4 hours. Employees should never learn about company news from Twitter.
**Crisis communication sequence:**
**Hour 0–4 (internal first):**
1. CEO or relevant leader sends an internal message
2. Acknowledge what happened factually
3. State what you know and what you don't know yet
4. Tell people what you're doing about it
5. Tell people what they should do if they're asked about it
**Hour 4–24 (external if needed):**
1. External statement (press, social) only if the event is public
2. Consistent with the internal message — same facts, audience-appropriate framing
3. Legal review if any claims or liability involved
**What not to do in a crisis:**
- Silence: letting rumors fill the vacuum
- Spin: people can detect it and it destroys trust
- "No comment": says "we have something to hide"
- Blaming: even if someone else caused the problem, your audience only cares what you're doing about it
**Template for crisis internal communication:**
> "Here's what happened: [factual description]. Here's what we know right now: [known facts]. Here's what we don't know yet: [honest uncertainty]. Here's what we're doing: [specific actions]. Here's what you should do if you're asked about this: [specific guidance]. I'll update you by [specific time] with more information."
---
## Narrative Consistency Checklist
Run before any major external communication:
- [ ] Is this consistent with what we told investors last month?
- [ ] Is this consistent with what we told employees at the last all-hands?
- [ ] Does this contradict anything on our website, careers page, or press releases?
- [ ] If an employee read this external communication, would they recognize the company being described?
- [ ] If an investor read our internal all-hands deck, would they find anything inconsistent?
- [ ] Have we been accurate about our current state, or are we projecting an aspiration?
---
## Key Questions for Narrative
- "Could a new employee explain to a friend why our company exists? What would they say?"
- "What do we tell investors about our strategy? What do we tell employees? Are these the same?"
- "If a journalist asked our team members to describe the company independently, what would they say?"
- "When did we last update our 'why we exist' story? Is it still true?"
- "What's the hardest question we'd get from each audience? Do we have an honest answer?"
## Red Flags
- Different departments describe the company mission differently
- Investor narrative emphasizes growth; employee narrative emphasizes stability (or vice versa)
- All-hands presentations are mostly slides, mostly one-way
- Q&A questions are screened or deflected
- Bad news reaches employees through Slack rumors before leadership communication
- Careers page describes a culture that employees don't recognize
## Integration with Other C-Suite Roles
| When... | Work with... | To... |
|---------|-------------|-------|
| Investor update prep | CFO | Align financial narrative with company narrative |
| Reorg or leadership change | CHRO + CEO | Sequence: employees first, then external |
| Product pivot | CPO | Align customer communication with investor story |
| Culture change | Culture Architect | Ensure internal story is consistent with external employer brand |
| M&A or partnership | CEO + COO | Control information flow, prevent narrative leaks |
| Crisis | All C-suite | Single voice, consistent story, internal first |
## Detailed References
- `references/narrative-frameworks.md` — Storytelling structures, founder narrative, bad news delivery, all-hands templates
- `templates/all-hands-template.md` — All-hands presentation template
FILE:internal-narrative/references/narrative-frameworks.md
# Narrative Frameworks
Reference frameworks for building compelling, consistent business narratives.
---
## 1. Storytelling Structure for Business
### The SCR Framework (Situation, Complication, Resolution)
Barbara Minto's Pyramid Principle adapted for business narrative. Works for any audience.
**Situation:** The established facts everyone agrees on.
**Complication:** What changed, what problem arose, what makes the situation untenable.
**Resolution:** What you're doing about it, and why this solution works.
**Example — Investor update:**
> **Situation:** We entered Q2 with €650K ARR and a target of €800K. Our DACH pipeline was strong at 3x coverage.
>
> **Complication:** Two large deals (€90K combined ARR) that were expected to close in May pushed to Q3 due to procurement delays on the customer side. We ended Q2 at €710K — below target but within the range we'd flag as manageable.
>
> **Resolution:** Both deals are now signed with June start dates. We're entering Q3 at €800K ARR. We've added a new procurement risk flag to our pipeline methodology to catch this pattern earlier.
**Why it works:** It respects the audience's intelligence, acknowledges the problem directly, and frames your response before they can object.
---
### The Problem-Solution-Evidence Structure
Best for pitches, product announcements, and strategy communications.
1. **The world as it is:** What's the current reality?
2. **What's broken about it:** Why is the status quo painful or inefficient?
3. **What we're doing:** Your specific solution
4. **Why it works:** Evidence, mechanism, or proof
5. **What happens next:** Call to action or forward look
**Example — All-hands strategy communication:**
> The world as it is: We have 80 customers in DACH. Churn is 8% annually. That means we're losing 6–7 customers a year just to stay flat.
>
> What's broken: Our onboarding takes 6 weeks. By week 4, customers haven't seen value yet and they're questioning the decision. We've traced 60% of churn to customers who never completed onboarding.
>
> What we're doing: We're redesigning onboarding to show the first meaningful mobility report within 48 hours of account activation.
>
> Why it will work: We ran this with 5 pilot customers in Q2. Time-to-first-value dropped from 4 weeks to 2 days. 4 of 5 expanded their contract within 60 days.
>
> What happens next: Engineering ships the new onboarding flow by August 15. CS is retrained by August 22. We'll run the new flow with all new customers from September 1 and report back at the October all-hands.
---
## 2. The Founder's Narrative
The founder's personal story is one of the most underutilized assets in a startup. Used well, it anchors the company's mission and creates genuine connection.
### The Founder Story Structure
**Origin:** What led you to this problem? (Ideally personal — you experienced it, someone you loved experienced it, you couldn't stop thinking about why nobody was solving it)
**Insight:** What did you see that others didn't? (Your unique perspective or unfair advantage)
**Decision:** The moment you committed. (Specific, not aspirational — "I left my job on March 14" not "I decided to pursue my passion")
**What you've learned:** 2–3 honest observations that shaped your approach. (Including what you got wrong)
**Where you're going:** Connection from your personal why to where the company is heading.
**Example (condensed):**
> My mother had a fall in 2018 that broke her hip. She spent 3 months in rehabilitation. The terrifying part: nobody saw it coming. Her doctor had assessed her fall risk 4 months earlier — using a paper questionnaire. I spent two years talking to geriatricians trying to understand why this assessment was still done by hand, on paper, in 2018. The answer: nobody had made it easy enough for a non-specialist to do it digitally. That's what we're building.
**Why it matters:** Investors, candidates, and customers all respond to a founder who started from a real problem rather than a market opportunity. The narrative makes you memorable and makes the mission credible.
---
## 3. How to Deliver Bad News Across Audiences
### Universal principles
1. **Internal first.** Always. Every time. No exceptions.
2. **Direct, not hedged.** "We missed our Q2 target by 12%" beats "Q2 performance came in below our expectations."
3. **Own it before explaining it.** Context comes after acknowledgment, not before.
4. **State what you're doing.** Bad news without a response plan creates panic.
5. **Give a timeline.** "We'll know more by [date]" is better than open-ended uncertainty.
### Delivering bad news to employees
**Format:** Synchronous (all-hands or team meeting), followed by written summary.
**What to say:**
- What happened (factual, no spin)
- What it means for the company
- What it means for them specifically (will roles change? Will comp change?)
- What you're doing about it
- When you'll have more information
**What not to say:**
- "I can't share the details" (share everything you legally can)
- "This is actually good news because..." (if it's bad news, don't reframe it before acknowledging it)
- "We saw this coming" (if you did, why didn't you tell them?)
**Example — Missed fundraise:**
> "I have to share news that's disappointing. We went out to raise a Series A in Q1, and we didn't close the round. We had term sheets that fell through when the market conditions shifted in April. We're not in crisis — we have 12 months of runway — but we need to recalibrate. Here's what that means concretely: we're pausing 3 open headcount. Everyone currently on the team keeps their role. We're going back to market in Q4 with stronger metrics. I'll share our updated financial model with everyone by Friday and answer every question you have."
---
### Delivering bad news to investors
**Format:** Written update (monthly update format) + proactive call if material.
**What to say:**
- Headline the bad news in the first paragraph (don't bury it)
- Context: what changed and what didn't change
- What you're doing about it
- What you need from them (if anything)
**What investors hate:**
- Finding out from someone other than you
- Bad news wrapped in so much context they have to work to find it
- "We're watching it closely" without specific action
- Consistent over-optimism followed by consistent misses
**Example — Investor update paragraph:**
> "Revenue miss: We ended Q2 at €710K ARR vs. a target of €800K. Two deals totaling €90K pushed to Q3 due to customer procurement delays (not product or relationship issues — both have since signed). We've adjusted our sales process to flag procurement risk earlier. Q3 is starting at €800K with those deals live."
---
### Delivering bad news to customers
**Scope:** Only share bad news that affects them. Don't share internal struggles that aren't relevant to their experience.
**Format:** Proactive communication from their account owner or a senior leader.
**What customers need:**
- What happened (that affects them)
- What you're doing about it
- What they should do (if anything)
- Who to contact
**What customers don't need:**
- Your internal financial struggles
- Drama about team changes
- More detail than affects their use of your product
**Example — Service disruption:**
> "Yesterday evening we experienced a 90-minute service outage that affected your access to [feature]. We've identified the root cause (a failed database migration) and deployed a fix. Your data is intact and complete. We've implemented additional monitoring to prevent this from recurring. I'd like to schedule a brief call to answer any questions you have."
---
## 4. Narrative Consistency Checklist
Use before any significant external communication.
### Pre-communication audit
**Factual consistency:**
- [ ] Is the ARR/revenue figure consistent with what we've shared with investors?
- [ ] Is the team size consistent with what's on LinkedIn and our careers page?
- [ ] Are our stated priorities consistent with our published roadmap?
- [ ] Is our "stage" description consistent across all channels? (We can't be "early stage" to investors and "established leader" to customers)
**Message consistency:**
- [ ] Does this message conflict with anything said in the last 90 days?
- [ ] If an employee read this external message, would they recognize the company?
- [ ] If an investor read our internal all-hands, would they find anything that contradicts what we've told them?
**Audience appropriateness:**
- [ ] Have we answered the key question for this specific audience?
- [ ] Have we avoided sharing information this audience doesn't need and shouldn't have?
- [ ] Have we framed the message for what this audience cares about — not what we want them to care about?
---
## 5. All-Hands Presentation Templates
See `templates/all-hands-template.md` for the complete slide-by-slide template.
### Monthly all-hands (30–45 min)
**Structure:**
1. State of the company (10 min) — honest, metric-driven
2. Progress on quarterly rocks (5 min) — on track / off track / done
3. Team spotlight (5 min) — one team's work, why it matters
4. What's coming next 30 days (5 min) — what to expect
5. Q&A (10–15 min) — real questions, real answers
### Quarterly all-hands (60–90 min)
**Structure:**
1. Last quarter results vs. targets (15 min)
2. What we learned (10 min) — honest reflection on what didn't work
3. Next quarter priorities (15 min) — company rocks, why these three
4. Strategy update (10 min) — anything changing? Why?
5. Team recognition (10 min) — specific, values-linked examples
6. Q&A (15–20 min)
### Annual all-hands (2–4 hours, often a full day)
**Structure:**
1. Year in review: what we achieved (30 min)
2. What we learned — what we'd do differently (20 min)
3. State of the company: financial health, competitive position (20 min)
4. 3-year vision update (30 min)
5. Next year's strategy and priorities (30 min)
6. Department presentations: what each team is building (60 min)
7. Celebrations and recognition (20 min)
8. Q&A + social (open-ended)
### The "no-BS questions" technique
At any all-hands, reserve the last 5 minutes for: "What question are you afraid to ask publicly? Submit anonymously via [link]."
Read 3–5 of the hardest ones out loud and answer them honestly. This builds more trust than 45 minutes of polished presentation.
FILE:internal-narrative/templates/all-hands-template.md
# All-Hands Presentation Template
**Monthly format (30–45 min) | Adjust timing for quarterly/annual**
---
## Slide 1: State of the Company
**Headline:** One honest sentence about where we are right now.
> "We're ahead on revenue, behind on hiring, and Q3 is looking strong."
**3 key metrics (vs. target):**
| Metric | Target | Actual | Status |
|--------|--------|--------|--------|
| ARR / Revenue | | | 🟢/🟡/🔴 |
| [Key growth metric] | | | |
| [Key health metric] | | | |
**One sentence on momentum:** Are we accelerating, steady, or facing headwinds? Be honest.
---
## Slide 2: Progress on Quarterly Rocks
For each company-level rock:
| Rock | Owner | Status |
|------|-------|--------|
| [Rock 1 description] | [Name] | ✅ Done / 🟡 On track / 🔴 At risk |
| [Rock 2 description] | [Name] | |
| [Rock 3 description] | [Name] | |
For any 🔴 at-risk rock: one sentence on what changed and what we're doing about it.
---
## Slide 3: What We're Proud Of
**One specific win from the last 30 days.**
Not the metric — the story behind it.
> "CS team saved the Müller Group account after a critical feature gap was flagged 72 hours before their renewal. They pulled together engineering, product, and sales in 24 hours and presented a roadmap commitment that converted a churned account into an expansion. That's what customer obsession looks like."
Tie to a company value. Name the people involved.
---
## Slide 4: What We Learned / What Didn't Work
**One honest thing that didn't go as planned.**
> "Our Q2 product launch was delayed 3 weeks because we underestimated the testing scope for the new export feature. We shipped it, customers are using it, but we learned that our pre-launch testing checklist needs to include third-party integration validation. We've added that to the template."
If it's small: 2 sentences. If it's big: more time here, less elsewhere.
**Why this slide exists:** A company that only celebrates wins teaches people to hide problems. This slide teaches people that honesty is valued.
---
## Slide 5: What's Coming Next 30 Days
**3 things to know about:**
1. [Upcoming release / launch / event] — [What it is and why it matters]
2. [Hiring update or org change] — [Honest current state]
3. [External event / partnership / market development] — [What we're watching]
**What NOT to include:** Vague aspirations. Only things people can actually act on or prepare for.
---
## Slide 6: Q&A
**Format:** Live questions preferred. Anonymous submission option always available.
**CEO rules for Q&A:**
- Answer the question asked, not the one you wish they'd asked
- "I don't know, but I'll find out and share by [date]" > vague answer
- "I can't share that yet because [reason], but I will when I can" > "no comment"
- If the same question has been asked three times across all-hands, it's a communication gap — fix it
**Closing line:**
> "Thanks for your time. If you have a question you didn't get to ask — Slack me directly, or use the anonymous form. I read every one."
---
## Presenter Notes
**Before every all-hands:**
- [ ] Review last all-hands deck — anything promised that wasn't delivered?
- [ ] Check: is there anything employees should have heard from us before this meeting?
- [ ] Have 3–5 real Q&A answers prepared for the hardest questions you'd expect
**During Q&A:**
- [ ] Don't deflect hard questions — they remember
- [ ] Don't over-explain — short answers signal confidence
- [ ] Don't let one person dominate — "let's take that to a 1:1" is a valid response
**After every all-hands:**
- [ ] Send a written summary within 24 hours (key metrics, decisions, answers to top questions)
- [ ] Follow up on any commitments made during Q&A within the stated timeframe
FILE:intl-expansion/SKILL.md
---
name: "intl-expansion"
description: "International market expansion strategy. Market selection, entry modes, localization, regulatory compliance, and go-to-market by region. Use when expanding to new countries, evaluating international markets, planning localization, or building regional teams."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: international-strategy
updated: 2026-03-05
---
# International Expansion
Frameworks for expanding into new markets: selection, entry, localization, and execution.
## Keywords
international expansion, market entry, localization, go-to-market, GTM, regional strategy, international markets, market selection, cross-border, global expansion
## Quick Start
**Decision sequence:** Market selection → Entry mode → Regulatory assessment → Localization plan → GTM strategy → Team structure → Launch.
## Market Selection Framework
### Scoring Matrix
| Factor | Weight | How to Assess |
|--------|--------|---------------|
| Market size (addressable) | 25% | TAM in target segment, willingness to pay |
| Competitive intensity | 20% | Incumbent strength, market gaps |
| Regulatory complexity | 20% | Barriers to entry, compliance cost, timeline |
| Cultural distance | 15% | Language, business practices, buying behavior |
| Existing traction | 10% | Inbound demand, existing customers, partnerships |
| Operational complexity | 10% | Time zones, infrastructure, payment systems |
### Entry Modes
| Mode | Investment | Control | Risk | Best For |
|------|-----------|---------|------|----------|
| **Export** (sell remotely) | Low | Low | Low | Testing demand |
| **Partnership** (reseller/distributor) | Medium | Medium | Medium | Markets with strong local requirements |
| **Local team** (hire in-market) | High | High | High | Strategic markets with proven demand |
| **Entity** (full subsidiary) | Very high | Full | High | Major markets, regulatory requirement |
| **Acquisition** | Highest | Full | Highest | Fast market entry with existing base |
**Default path:** Export → Partnership → Local team → Entity (graduate as revenue justifies).
## Localization Checklist
### Product
- [ ] Language (UI, documentation, support content)
- [ ] Currency and pricing (local pricing, not just conversion)
- [ ] Payment methods (varies wildly by market)
- [ ] Date/time/number formats
- [ ] Legal requirements (data residency, privacy)
- [ ] Cultural adaptation (not just translation)
### Go-to-Market
- [ ] Messaging adaptation (what resonates locally)
- [ ] Channel strategy (channels differ by market)
- [ ] Local case studies and social proof
- [ ] Local partnerships and integrations
- [ ] Event/conference presence
- [ ] Local SEO and content
### Operations
- [ ] Legal entity (if required)
- [ ] Tax compliance
- [ ] Employment law (if hiring locally)
- [ ] Customer support (hours, language)
- [ ] Banking and payments
## Key Questions
- "Is there pull from the market, or are we pushing?"
- "What's the cost of entry vs the 3-year revenue opportunity?"
- "Can we serve this market from HQ, or do we need boots on the ground?"
- "What's the regulatory timeline? Can we launch before the paperwork is done?"
- "Who's winning in this market and what would it take to displace them?"
## Common Mistakes
| Mistake | Why It Happens | Prevention |
|---------|---------------|------------|
| Entering too many markets at once | FOMO, board pressure | Max 1-2 new markets per year |
| Copy-paste GTM from home market | Assuming buyers are the same | Research local buying behavior |
| Underestimating regulatory cost | "We'll figure it out" | Regulatory assessment BEFORE committing |
| Hiring too early | Optimism | Prove demand before hiring local team |
| Wrong pricing (just converting) | Laziness | Research willingness to pay locally |
## Integration with C-Suite Roles
| Role | Contribution |
|------|-------------|
| CEO | Market selection, strategic commitment |
| CFO | Investment sizing, ROI modeling, entity structure |
| CRO | Revenue targets, sales model adaptation |
| CMO | Positioning, channel strategy, local brand |
| CPO | Localization roadmap, feature priorities |
| CTO | Infrastructure, data residency, scaling |
| CHRO | Local hiring, employment law, comp |
| COO | Operations setup, process adaptation |
## Resources
- `references/market-entry-playbook.md` — detailed entry playbook by market type
- `references/regional-guide.md` — specific considerations for key regions (EU, US, APAC, LATAM)
FILE:intl-expansion/references/market-entry-playbook.md
# Market Entry Playbook
Step-by-step framework for entering a new international market.
## Phase 0: Validation (4-8 weeks)
Before committing resources, validate demand:
### Signal Assessment
| Signal | Strength | Action |
|--------|----------|--------|
| Inbound inquiries from the market | Strong | Fast-track evaluation |
| Existing customers using from that market | Strong | Interview them, understand needs |
| Competitor succeeding there | Medium | Market exists, but competition too |
| Partner referral | Medium | Validate independently |
| Market research says it's big | Weak | Research ≠ demand |
| Board says "we should be in X" | Weakest | Push back with data |
### Lightweight Validation
1. **Landing page test** — localized landing page with waitlist
2. **Ad spend test** — $2-5K in targeted ads, measure conversion
3. **Sales outreach** — 20 calls to potential customers in market
4. **Partner conversations** — 3-5 potential local partners
5. **Competitor analysis** — who's there, what they charge, customer reviews
**Pass criteria:** At least 2 of: qualified pipeline > $50K, waitlist > 100, partner willing to co-sell.
## Phase 1: Planning (4-6 weeks)
### Market-Specific GTM
| Element | Home Market | New Market | Notes |
|---------|------------|------------|-------|
| ICP | [your ICP] | [adapted ICP] | May be different segment |
| Pricing | [home price] | [local price] | Value-based, not conversion |
| Channels | [home channels] | [local channels] | Research what works locally |
| Sales model | [home model] | [adapted model] | Self-serve may not work everywhere |
| Support | [home support] | [local support] | Language, hours, expectations |
### Pricing Strategy by Market
- **Developed markets (US, UK, DACH, Nordics):** Price for value, premium positioning
- **Growth markets (Southern Europe, Eastern Europe):** 20-40% discount from core market
- **Emerging markets (LATAM, SEA):** 40-60% discount or different packaging
- **Enterprise everywhere:** Don't discount — add local value instead
### Regulatory Pre-Work
1. Data residency requirements (where must data live?)
2. Industry-specific regulations (healthcare, finance, education)
3. Tax obligations (VAT, withholding, nexus)
4. Employment law basics (if hiring)
5. Import/export restrictions (if applicable)
6. Timeline to compliance (weeks, months, years?)
## Phase 2: Entry (8-12 weeks)
### Minimum Viable Presence
| Element | MVP | Full | When to Upgrade |
|---------|-----|------|-----------------|
| Legal entity | None (sell cross-border) | Local subsidiary | Revenue > $500K/year |
| Team | Remote sales + support | Local office | > 5 local employees |
| Product | English + key translations | Full localization | Customer feedback demands it |
| Payments | International card processing | Local payment methods | Conversion drops |
| Support | Home team covers (extended hours) | Local support team | Volume requires it |
### Launch Sequence
1. **Week 1-2:** Product localization (minimum viable)
2. **Week 3-4:** Local pricing and payment setup
3. **Week 5-6:** Marketing launch (content, ads, PR)
4. **Week 7-8:** Sales activation (outreach, partner launch)
5. **Week 9-12:** Iterate based on first customers
### First 10 Customers
These are your foundation. Over-invest in their success:
- Weekly check-ins for first 90 days
- Dedicated support contact
- Feedback loop to product team
- Case study development
- Referral program
## Phase 3: Scale (6-12 months)
### When to Invest More
| Signal | Action |
|--------|--------|
| Pipeline > 3x capacity | Hire more sales |
| Support tickets in local language > 30% | Hire local support |
| Regulatory requirement for local entity | Establish subsidiary |
| Revenue > $500K ARR from market | Appoint country manager |
| 3+ enterprise deals require local presence | Open local office |
### Country Manager Profile
First local hire matters enormously:
- **Must have:** Domain expertise, local network, startup mentality
- **Nice to have:** Experience with your type of product
- **Red flag:** Wants to build a big team immediately
- **Ideal:** Someone who can sell, support, and partner — a generalist
### Common Scaling Mistakes
1. **Hiring a country manager too early** — Before product-market fit in that market
2. **Building a full local team before proving the model** — Expensive and hard to unwind
3. **Letting the local team operate independently** — They need to integrate, not isolate
4. **Ignoring local competition** — They know the market better than you
5. **Applying home-market playbook** — What works in the US may fail in Germany
## Market Type Playbooks
### Expanding Within Europe (DACH → EU)
- Regulatory: GDPR already covers you, but check industry-specific
- Languages: English works for Nordics/Netherlands, but not for France/Spain/Italy
- Pricing: PPP varies less within EU, but willingness to pay differs
- Sales: Direct works for DACH/Nordics, partner-heavy for Southern Europe
- Fastest path: UK → Nordics → Benelux → France → Spain → Italy
### Entering the US from Europe
- Legal: Delaware C-Corp for investment compatibility
- Sales: Everything is bigger — territories, deal sizes, expectations
- Pricing: Usually 20-30% higher than Europe
- Support: US customers expect fast response, US business hours
- Competition: More competitors, but also more budget
- Entry: Start with coast (NYC or SF), not middle America
### Entering APAC
- Diversity: APAC is not one market — it's 20+
- Start: Singapore (English, business-friendly) or Australia
- Japan/Korea: Need local partner, high localization bar
- India: Large market, price-sensitive, relationship-driven
- China: Separate strategy entirely, regulatory complexity extreme
## Measuring Success
| Metric | Month 3 Target | Month 6 Target | Month 12 Target |
|--------|---------------|----------------|-----------------|
| Pipeline | 10x of revenue target | 5x of revenue target | 3x of revenue target |
| Customers | 5-10 | 20-50 | 50-100+ |
| ARR | $50-100K | $200-500K | $500K-1M |
| NPS | > 30 | > 40 | > 50 |
| Churn | < 5% monthly | < 3% monthly | < 2% monthly |
Metrics should improve each quarter. If they flatten, something's wrong with product-market fit in that specific market.
FILE:intl-expansion/references/regional-guide.md
# Regional Expansion Guide
Specific considerations for key regions. Not exhaustive — these are the patterns that trip up most expanding companies.
## Europe
### DACH (Germany, Austria, Switzerland)
- **Language:** German required for SMB. Enterprise sometimes English.
- **Sales:** Relationship-driven, longer cycles, value formal proposals
- **Pricing:** Willing to pay premium for quality and reliability
- **Compliance:** GDPR, industry-specific (MDR for medical devices, BaFin for finance)
- **Payment:** SEPA, invoice preferred for B2B (not credit cards)
- **Culture:** Punctuality matters. Directness is respected. Don't oversell.
- **Data:** Strong preference for EU data residency
- **Entity:** GmbH for subsidiary, typically €25K minimum capital
### Nordics (Sweden, Norway, Denmark, Finland)
- **Language:** English widely accepted in business
- **Sales:** Consensus-driven decisions, flat hierarchies
- **Pricing:** High willingness to pay, value innovation
- **Compliance:** GDPR, strong data protection culture
- **Culture:** Equality-focused, sustainability matters, low-key approach preferred
- **Entry:** Often the easiest European expansion for English-speaking companies
### France
- **Language:** French required, even for enterprise (most buyers prefer it)
- **Sales:** Formal, hierarchical decision-making, relationships matter
- **Pricing:** Price-sensitive but willing to invest in proven solutions
- **Compliance:** GDPR + CNIL (strict data authority), French hosting preference
- **Culture:** Business lunches are real meetings. Email etiquette matters.
- **Entity:** SAS or SARL, complex employment law
### UK
- **Language:** English (obviously)
- **Sales:** Similar to US but smaller deal sizes
- **Pricing:** Competitive market, price comparisons common
- **Compliance:** UK GDPR (post-Brexit), FCA for finance
- **Culture:** Understated, humor works, don't be too pushy
- **Post-Brexit:** Separate data adequacy, some regulatory divergence
### Southern Europe (Spain, Italy, Portugal)
- **Language:** Local language strongly preferred
- **Sales:** Relationship-heavy, trust-based, longer cycles
- **Pricing:** Lower willingness to pay than Northern Europe
- **Entry:** Partner/reseller model often more effective than direct
- **Culture:** Personal relationships precede business relationships
- **Timing:** August is essentially closed in many industries
### Eastern Europe (Poland, Czech Republic, Romania)
- **Language:** Local language for SMB, English for enterprise/tech
- **Sales:** Growing market, value-conscious, quick adoption of new tech
- **Pricing:** 30-50% of Western European pricing
- **Talent:** Excellent engineering talent for local offices
- **Entry:** Often good for first offshore team, not just sales
## United States
### General
- **Entity:** Delaware C-Corp if seeking US investment
- **Sales:** Expect American-style responsiveness (same-day replies)
- **Pricing:** Higher than Europe (typically 20-40%)
- **Compliance:** State-by-state complexity (privacy, tax nexus)
- **Culture:** Optimistic, results-oriented, comfortable with direct outreach
- **Legal:** More litigious environment, good contracts essential
### Regional Differences
| Region | Characteristics |
|--------|----------------|
| **West Coast** | Tech-forward, early adopters, startup-friendly |
| **East Coast** | Enterprise-heavy, finance and healthcare strong |
| **Midwest** | Manufacturing, agriculture, relationship-driven, underserved |
| **South** | Growing tech hubs (Austin, Atlanta, Nashville), cost-conscious |
### Key Considerations
- Sales tax: Complex, state-dependent, use automation (Stripe Tax, Avalara)
- Privacy: California (CCPA/CPRA), Virginia, Colorado, Connecticut have state laws
- Employment: At-will, but benefits expectations are high
- Health insurance: Expected by employees (significant cost)
## APAC
### Singapore
- **Best entry point for APAC** (English, business-friendly, strong rule of law)
- Low tax, easy incorporation, access to Southeast Asian markets
- Small domestic market — use as hub, not primary market
### Australia
- **English-speaking, familiar business culture** (similar to UK)
- Strong B2B market, good for SaaS
- Data privacy: Australian Privacy Act
- Time zones: Challenge for support from Europe
### Japan
- **Highest quality bar in the world** — products must be polished
- Local partner essential (trust, introductions, support)
- Japanese localization is non-negotiable
- Long sales cycles but very loyal once committed
- Business etiquette matters significantly
### India
- **Huge market but price-sensitive**
- Strong engineering talent market
- Relationship-driven, patience required
- UPI and local payment methods essential
- Often better as talent market than sales market initially
## LATAM
### General
- Portuguese (Brazil) and Spanish (rest) — two distinct markets
- Growing SaaS adoption, especially in Brazil, Mexico, Colombia
- Price-sensitive but growing willingness to pay for quality
- Boleto (Brazil) and local payment methods essential
- Currency volatility can affect pricing strategy
### Brazil
- Largest LATAM market by far
- Complex tax system (NF-e, ICMS, PIS/COFINS)
- Portuguese required, no exceptions
- Strong startup ecosystem (São Paulo)
- Data privacy: LGPD (similar to GDPR)
### Mexico
- Second largest LATAM market
- Growing US business ties
- Spanish required
- Proximity to US is strategic advantage
- Increasing SaaS adoption
## Cross-Region Patterns
### What Works Everywhere
- Start with existing customer demand (pull, not push)
- Invest in local language support before local sales
- Price for the market, not for your cost structure
- Build local case studies as fast as possible
- Find one strong local partner before hiring
### What Never Works
- Assuming English is enough (even when people speak it)
- Copy-pasting marketing materials with just translation
- Ignoring local payment preferences
- Treating "Europe" or "APAC" as single markets
- Sending your best home-market rep without local context
FILE:ma-playbook/SKILL.md
---
name: "ma-playbook"
description: "M&A strategy for acquiring companies or being acquired. Due diligence, valuation, integration, and deal structure. Use when evaluating acquisitions, preparing for acquisition, M&A due diligence, integration planning, or deal negotiation."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: ma-strategy
updated: 2026-03-05
---
# M&A Playbook
Frameworks for both sides of M&A: acquiring companies and being acquired.
## Keywords
M&A, mergers and acquisitions, due diligence, acquisition, acqui-hire, integration, deal structure, valuation, LOI, term sheet, earnout
## Quick Start
**Acquiring:** Start with strategic rationale → target screening → due diligence → valuation → negotiation → integration.
**Being Acquired:** Start with readiness assessment → data room prep → advisor selection → negotiation → transition.
## When You're Acquiring
### Strategic Rationale (answer before anything else)
- **Buy vs Build:** Can you build this faster/cheaper? If yes, don't acquire.
- **Acqui-hire vs Product vs Market:** What are you really buying? Talent? Technology? Customers?
- **Integration complexity:** How hard is it to merge this into your company?
### Due Diligence Checklist
| Domain | Key Questions | Red Flags |
|--------|--------------|-----------|
| Financial | Revenue quality, customer concentration, burn rate | >30% revenue from 1 customer |
| Technical | Code quality, tech debt, architecture fit | Monolith with no tests |
| Legal | IP ownership, pending litigation, contracts | Key IP owned by individuals |
| People | Key person risk, culture fit, retention risk | Founders have no lockup/earnout |
| Market | Market position, competitive threats | Declining market share |
| Customers | Churn rate, NPS, contract terms | High churn, short contracts |
### Valuation Approaches
- **Revenue multiple:** Industry-dependent (2-15x ARR for SaaS)
- **Comparable transactions:** What similar companies sold for
- **DCF:** For profitable companies only (most startups: use multiples)
- **Acqui-hire:** $1-3M per engineer in hot markets
### Integration Frameworks
See `references/integration-playbook.md` for the 100-day integration plan.
## When You're Being Acquired
### Readiness Signals
- Inbound interest from strategic buyers
- Market consolidation happening around you
- Fundraising becomes harder than operating
- Founder ready for a transition
### Preparation (6-12 months before)
1. Clean up financials (audited if possible)
2. Document all IP and contracts
3. Reduce customer concentration
4. Lock up key employees
5. Build the data room
6. Engage an M&A advisor
### Negotiation Points
| Term | What to Watch | Your Leverage |
|------|--------------|---------------|
| Valuation | Earnout traps (unreachable targets) | Multiple competing offers |
| Earnout | Milestone definitions, measurement period | Cash-heavy vs earnout-heavy split |
| Lockup | Duration, conditions | Your replaceability |
| Rep & warranties | Scope of liability | Escrow vs indemnification cap |
| Employee retention | Who gets offers, at what terms | Key person dependencies |
## Red Flags (Both Sides)
- No clear strategic rationale beyond "it's a good deal"
- Culture clash visible during due diligence and ignored
- Key people not locked in before close
- Integration plan doesn't exist or is "we'll figure it out"
- Valuation based on projections, not actuals
## Integration with C-Suite Roles
| Role | Contribution to M&A |
|------|-------------------|
| CEO | Strategic rationale, negotiation lead |
| CFO | Valuation, deal structure, financing |
| CTO | Technical due diligence, integration architecture |
| CHRO | People due diligence, retention planning |
| COO | Integration execution, process merge |
| CPO | Product roadmap impact, customer overlap |
## Resources
- `references/integration-playbook.md` — 100-day post-acquisition integration plan
- `references/due-diligence-checklist.md` — comprehensive DD checklist by domain
FILE:ma-playbook/references/due-diligence-checklist.md
# M&A Due Diligence Checklist
Comprehensive due diligence organized by domain. Not every item applies to every deal — focus on what matters for YOUR acquisition rationale.
## Financial Due Diligence
### Revenue Quality
- [ ] Revenue by customer (top 10 customer concentration)
- [ ] Revenue by product line
- [ ] Revenue by geography
- [ ] MRR/ARR trend (24 months minimum)
- [ ] Churn rate (gross and net, by cohort)
- [ ] Revenue recognition policies
- [ ] Deferred revenue / backlog
- [ ] One-time vs recurring revenue split
- [ ] Professional services vs product revenue
### Profitability
- [ ] Gross margin by product line
- [ ] Operating expenses breakdown
- [ ] Burn rate trend (improving or worsening?)
- [ ] Path to profitability (realistic or aspirational?)
- [ ] Unit economics (LTV, CAC, payback by channel)
### Cash & Liabilities
- [ ] Cash position and burn rate
- [ ] Outstanding debt (terms, covenants)
- [ ] Accounts receivable aging
- [ ] Accounts payable
- [ ] Pending or contingent liabilities
- [ ] Tax obligations (any back taxes?)
- [ ] Cap table (fully diluted, option pool)
### Financial Controls
- [ ] Audit history (audited vs reviewed vs compiled)
- [ ] Financial reporting cadence and quality
- [ ] Budget vs actual variance history
- [ ] Key financial policies
## Technical Due Diligence
### Architecture
- [ ] Architecture diagrams (current state)
- [ ] Technology stack inventory
- [ ] Infrastructure (cloud provider, regions, costs)
- [ ] Scalability assessment (current capacity vs load)
- [ ] Security architecture (encryption, access controls)
### Code Quality
- [ ] Test coverage (unit, integration, e2e)
- [ ] CI/CD pipeline maturity
- [ ] Technical debt inventory (estimated remediation cost)
- [ ] Code review practices
- [ ] Documentation quality
### Data
- [ ] Data architecture and storage
- [ ] Data privacy compliance (GDPR, CCPA)
- [ ] Data portability (can you migrate it?)
- [ ] Proprietary data assets (training data, user data)
- [ ] Data retention policies
### Operational
- [ ] Uptime history (SLA compliance)
- [ ] Incident history (frequency, severity, resolution time)
- [ ] Monitoring and alerting coverage
- [ ] Disaster recovery plan and testing history
- [ ] On-call rotation and processes
## Legal Due Diligence
### Intellectual Property
- [ ] Patents (granted and pending)
- [ ] Trademarks
- [ ] Copyright registrations
- [ ] IP assignment agreements (all employees/contractors)
- [ ] Open source usage and compliance
- [ ] Trade secrets protection measures
### Contracts
- [ ] Customer contracts (terms, renewals, termination rights)
- [ ] Vendor contracts (key dependencies, terms)
- [ ] Partnership agreements
- [ ] Lease agreements
- [ ] Employment agreements (non-competes, IP clauses)
### Compliance & Litigation
- [ ] Pending or threatened litigation
- [ ] Regulatory compliance status
- [ ] Government investigations
- [ ] Insurance coverage
- [ ] Prior legal disputes and resolutions
## People Due Diligence
### Team Composition
- [ ] Org chart with roles and tenure
- [ ] Key person dependencies (bus factor)
- [ ] Compensation details (salary, equity, bonuses)
- [ ] Employment agreements and non-competes
- [ ] Contractor vs employee classification
### Culture & Retention
- [ ] Recent engagement survey results
- [ ] Turnover rate (last 12-24 months)
- [ ] Glassdoor/reputation assessment
- [ ] Management quality assessment
- [ ] Culture compatibility analysis
### HR Compliance
- [ ] Employee handbook and policies
- [ ] HR complaints or investigations
- [ ] Benefits programs
- [ ] Equity plan details and administration
## Market Due Diligence
### Market Position
- [ ] Market size (TAM, SAM, SOM) with sources
- [ ] Market share estimate
- [ ] Growth rate (market and company)
- [ ] Competitive landscape (direct and indirect)
- [ ] Barriers to entry / competitive moat
### Customer Analysis
- [ ] Customer segmentation
- [ ] Win/loss analysis (why customers chose them)
- [ ] NPS or satisfaction scores
- [ ] Customer acquisition channels
- [ ] Customer lifetime and expansion patterns
## Red Flag Severity Guide
| Severity | Examples | Action |
|----------|---------|--------|
| **Deal killer** | IP not properly assigned, undisclosed litigation, fraud | Walk away |
| **Major renegotiation** | Customer concentration >40%, key person risk, technical debt >6 months | Reduce price or add protections |
| **Integration risk** | Culture mismatch, legacy systems, manual processes | Budget for remediation |
| **Monitor** | High churn, declining NPS, aging tech stack | Track post-close |
## Due Diligence Timeline
| Phase | Duration | Focus |
|-------|----------|-------|
| Preliminary | 1-2 weeks | Public info, financials, high-level tech |
| Deep dive | 4-6 weeks | All domains, interviews, code review |
| Confirmation | 1-2 weeks | Verify claims, resolve open questions |
| Final | 1 week | Legal review, final terms negotiation |
FILE:ma-playbook/references/integration-playbook.md
# Post-Acquisition Integration Playbook
The 100-day plan for integrating an acquisition. Most acquisitions fail not because of bad deals but bad integration.
## The Integration Paradox
Move too fast → you break what you bought.
Move too slow → talent leaves, customers churn, value evaporates.
**The rule:** Decide on day 1 what stays separate and what merges. Then execute without wavering.
## Pre-Close (Day -30 to 0)
### Integration Lead
- Appoint ONE integration lead (not a committee)
- This person reports to the CEO, has authority over all workstreams
- Full-time role for 100 days minimum
### Planning
| Workstream | Owner | Day 1 Decisions |
|-----------|-------|-----------------|
| People | CHRO | Who stays, comp alignment, reporting lines |
| Technology | CTO | Systems to merge, timeline, migration order |
| Customers | CRO | Communication plan, account ownership |
| Product | CPO | Roadmap integration, feature consolidation |
| Operations | COO | Process alignment, tool consolidation |
| Finance | CFO | Entity structure, billing, reporting |
| Legal | External | Contract assignments, IP transfer |
### Communication Plan (ready before close)
- Employee announcement (both companies) — Day 1
- Customer notification — Day 1-3
- Partner/vendor notification — Week 1
- Public announcement — per deal terms
## Week 1 (Days 1-7): Stabilize
**Goal:** No one leaves, no customer churns, operations continue.
- [ ] All-hands meeting (both companies together)
- [ ] 1:1 with every acquired leader (within 48 hours)
- [ ] Retention packages confirmed for key employees
- [ ] Customer communication sent (personal for top 20 accounts)
- [ ] Systems access provisioned (email, Slack, tools)
- [ ] Integration FAQ published internally
### The First All-Hands
What people want to hear:
1. Why this happened (honest version)
2. What changes (be specific, not vague)
3. What doesn't change (equally important)
4. Their job security (be direct)
5. Timeline for decisions
What NOT to say: "Nothing will change." (It will. They know it.)
## Month 1 (Days 1-30): Orient
**Goal:** Teams know each other, quick wins shipped, blockers identified.
### People
- [ ] Org chart finalized and communicated
- [ ] Comp band alignment completed
- [ ] Benefits transition timeline published
- [ ] Cross-team introductions facilitated (not forced)
- [ ] Culture assessment: what's different, what's compatible
### Technology
- [ ] Architecture assessment complete
- [ ] Migration priority ranked (quick wins first)
- [ ] Shared development environment established
- [ ] Code access and permissions set up
- [ ] Technical debt from both sides documented
### Customers
- [ ] Top 20 accounts contacted personally by leadership
- [ ] Unified support channel established (or plan for it)
- [ ] Pricing/contract transition plan for overlapping customers
- [ ] Product roadmap communication (what's coming, what's being deprecated)
### Quick Wins
Ship something visible in the first 30 days. A feature that combines both companies' strengths. This proves the acquisition works better than any memo.
## Month 2-3 (Days 31-100): Integrate
**Goal:** Core systems merged, one team operating, value creation visible.
### Systems Integration Priority
1. **Communication** (Slack, email) — Week 2
2. **Identity** (SSO, accounts) — Week 3
3. **Development** (repos, CI/CD) — Month 1
4. **Data** (analytics, CRM) — Month 2
5. **Product** (shared platform) — Month 2-3
6. **Finance** (billing, reporting) — Month 3
### Culture Integration
- **Don't:** Force the acquired team to adopt everything immediately
- **Do:** Find the best practices from BOTH cultures, adopt the winner
- **Don't:** Rename everything on Day 1
- **Do:** Co-create the combined identity over 60 days
- **Watch for:** "Us vs them" language, meeting exclusions, information hoarding
### Measuring Integration Success
| Metric | Target | Frequency |
|--------|--------|-----------|
| Employee retention (key people) | > 90% at 100 days | Weekly |
| Customer retention | > 95% at 100 days | Monthly |
| Cross-team collaboration (PRs, meetings) | Increasing trend | Weekly |
| Synergy revenue (combined offerings) | First deal within 60 days | Monthly |
| Integration milestones hit | > 80% on time | Weekly |
## Post-100 Days
Integration isn't "done" at 100 days. But the foundation should be solid.
### Ongoing
- Quarterly integration retrospective (what's working, what isn't)
- Culture health check at 6 months
- Full financial integration assessment at 12 months
- Earnout milestone tracking (if applicable)
### Common Failure Modes
| Failure | Root Cause | Prevention |
|---------|-----------|------------|
| Key talent leaves at month 4 | Retention cliff, culture mismatch | Longer earnout, culture attention |
| Customer churn spike at month 6 | Product changes without warning | Over-communicate product roadmap |
| "Two companies in a trenchcoat" | Incomplete integration | Force cross-functional projects |
| Value never materializes | Wrong acquisition rationale | Kill the deal if rationale was wrong |
| Acquirer culture overwhelms | "Our way is the only way" | Adopt best of both explicitly |
## The Kill Switch
Sometimes acquisitions don't work. Signs it's failing:
- Key people leaving despite retention packages
- Customers churning above baseline
- Integration milestones consistently missed
- Culture clash worsening, not improving
- Revenue synergies aren't materializing at month 6
**Options:**
1. Double down with new integration lead and plan
2. Operate as semi-autonomous unit (less integration)
3. Spin off or divest (expensive, but sometimes necessary)
Admitting failure early costs less than dragging it out.
FILE:org-health-diagnostic/SKILL.md
---
name: "org-health-diagnostic"
description: "Cross-functional organizational health check combining signals from all C-suite roles. Scores 8 dimensions on a traffic-light scale with drill-down recommendations. Use when assessing overall company health, preparing for board reviews, identifying at-risk functions, or when user mentions org health, health check, or health dashboard."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: organizational-health
updated: 2026-03-05
python-tools: health_scorer.py
frameworks: health-benchmarks
---
# Org Health Diagnostic
Eight dimensions. Traffic lights. Real benchmarks. Surfaces the problems you don't know you have.
## Keywords
org health, organizational health, health diagnostic, health dashboard, health check, company health, functional health, team health, startup health, health scorecard, health assessment, risk dashboard
## Quick Start
```bash
python scripts/health_scorer.py # Guided CLI — enter metrics, get scored dashboard
python scripts/health_scorer.py --json # Output raw JSON for integration
```
Or describe your metrics:
```
/health [paste your key metrics or answer prompts]
/health:dimension [financial|revenue|product|engineering|people|ops|security|market]
```
## The 8 Dimensions
### 1. 💰 Financial Health (CFO)
**What it measures:** Can we fund operations and invest in growth?
Key metrics:
- **Runway** — months at current burn (Green: >12, Yellow: 6-12, Red: <6)
- **Burn multiple** — net burn / net new ARR (Green: <1.5x, Yellow: 1.5-2.5x, Red: >2.5x)
- **Gross margin** — SaaS target: >65% (Green: >70%, Yellow: 55-70%, Red: <55%)
- **MoM growth rate** — contextual by stage (see benchmarks)
- **Revenue concentration** — top customer % of ARR (Green: <15%, Yellow: 15-25%, Red: >25%)
### 2. 📈 Revenue Health (CRO)
**What it measures:** Are customers staying, growing, and recommending us?
Key metrics:
- **NRR (Net Revenue Retention)** — Green: >110%, Yellow: 100-110%, Red: <100%
- **Logo churn rate (annualized)** — Green: <5%, Yellow: 5-10%, Red: >10%
- **Pipeline coverage (next quarter)** — Green: >3x, Yellow: 2-3x, Red: <2x
- **CAC payback period** — Green: <12 months, Yellow: 12-18, Red: >18 months
- **Average ACV trend** — directional: growing, flat, declining
### 3. 🚀 Product Health (CPO)
**What it measures:** Do customers love and use the product?
Key metrics:
- **NPS** — Green: >40, Yellow: 20-40, Red: <20
- **DAU/MAU ratio** — engagement proxy (Green: >40%, Yellow: 20-40%, Red: <20%)
- **Core feature adoption** — % of users using primary value feature (Green: >60%)
- **Time-to-value** — days from signup to first core action (lower is better)
- **Customer satisfaction (CSAT)** — Green: >4.2/5, Yellow: 3.5-4.2, Red: <3.5
### 4. ⚙️ Engineering Health (CTO)
**What it measures:** Can we ship reliably and sustain velocity?
Key metrics:
- **Deployment frequency** — Green: daily, Yellow: weekly, Red: monthly or less
- **Change failure rate** — % of deployments causing incidents (Green: <5%, Red: >15%)
- **Mean time to recovery (MTTR)** — Green: <1 hour, Yellow: 1-4 hours, Red: >4 hours
- **Tech debt ratio** — % of sprint capacity on debt (Green: <20%, Yellow: 20-35%, Red: >35%)
- **Incident frequency** — P0/P1 per month (Green: <2, Yellow: 2-5, Red: >5)
### 5. 👥 People Health (CHRO)
**What it measures:** Is the team stable, engaged, and growing?
Key metrics:
- **Regrettable attrition (annualized)** — Green: <10%, Yellow: 10-20%, Red: >20%
- **Engagement score** — (eNPS or similar; Green: >30, Yellow: 0-30, Red: <0)
- **Time-to-fill (avg days)** — Green: <45, Yellow: 45-90, Red: >90
- **Manager-to-IC ratio** — Green: 1:5–1:8, Yellow: 1:3–1:5 or 1:8–1:12, Red: outside
- **Internal promotion rate** — at least 25-30% of senior roles filled internally
### 6. 🔄 Operational Health (COO)
**What it measures:** Are we executing our strategy with discipline?
Key metrics:
- **OKR completion rate** — % of key results hitting target (Green: >70%, Yellow: 50-70%, Red: <50%)
- **Decision cycle time** — days from decision needed to decision made (Green: <48h, Yellow: 48h-1w)
- **Meeting effectiveness** — % of meetings with clear outcome (qualitative)
- **Process maturity** — level 1-5 scale (see COO advisor)
- **Cross-functional initiative completion** — % on time, on scope
### 7. 🔒 Security Health (CISO)
**What it measures:** Are we protecting customers and maintaining compliance?
Key metrics:
- **Security incidents (last 90 days)** — Green: 0, Yellow: 1-2 minor, Red: 1+ major
- **Compliance status** — certifications current/in-progress vs. overdue
- **Vulnerability remediation SLA** — % of critical CVEs patched within SLA (Green: 100%)
- **Security training completion** — % of team current (Green: >95%)
- **Pen test recency** — Green: <12 months, Yellow: 12-24, Red: >24 months
### 8. 📣 Market Health (CMO)
**What it measures:** Are we winning in the market and growing efficiently?
Key metrics:
- **CAC trend** — improving, flat, or worsening QoQ
- **Organic vs paid lead mix** — more organic = healthier (less fragile)
- **Win rate** — % of qualified opportunities closed-won (Green: >25%, Yellow: 15-25%, Red: <15%)
- **Competitive win rate** — against primary competitors specifically
- **Brand NPS** — awareness + preference scores in ICP
---
## Scoring & Traffic Lights
Each dimension is scored 1-10 with traffic light:
- 🟢 **Green (7-10):** Healthy — maintain and optimize
- 🟡 **Yellow (4-6):** Watch — trend matters; improving or declining?
- 🔴 **Red (1-3):** Action required — address within 30 days
**Overall Health Score:**
Weighted average by company stage (see `references/health-benchmarks.md` for weights).
---
## Dimension Interactions (Why One Problem Creates Another)
| If this dimension is red... | Watch these dimensions next |
|-----------------------------|----------------------------|
| Financial Health | People (freeze hiring) → Engineering (freeze infra) → Product (cut scope) |
| Revenue Health | Financial (cash gap) → People (attrition risk) → Market (lose positioning) |
| People Health | Engineering (velocity drops) → Product (quality drops) → Revenue (churn rises) |
| Engineering Health | Product (features slip) → Revenue (deals stall on product) |
| Product Health | Revenue (NRR drops, churn rises) → Market (CAC rises; referrals dry up) |
| Operational Health | All dimensions degrade over time (execution failure cascades everywhere) |
---
## Dashboard Output Format
```
ORG HEALTH DIAGNOSTIC — [Company] — [Date]
Stage: [Seed/A/B/C] Overall: [Score]/10 Trend: [↑ Improving / → Stable / ↓ Declining]
DIMENSION SCORES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💰 Financial 🟢 8.2 Runway 14mo, burn 1.6x — strong
📈 Revenue 🟡 5.8 NRR 104%, pipeline thin (1.8x coverage)
🚀 Product 🟢 7.4 NPS 42, DAU/MAU 38%
⚙️ Engineering 🟡 5.2 Debt at 30%, MTTR 3.2h
👥 People 🔴 3.8 Attrition 24%, eng morale low
🔄 Operations 🟡 6.0 OKR 65% completion
🔒 Security 🟢 7.8 SOC 2 Type II complete, 0 incidents
📣 Market 🟡 5.5 CAC rising, win rate dropped to 22%
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TOP PRIORITIES
🔴 [1] People: attrition at 24% — engineering velocity will drop in 60 days
Action: CHRO + CEO to run retention audit; target top 5 at-risk this week
🟡 [2] Revenue: pipeline coverage at 1.8x — Q+1 miss risk is high
Action: CRO to add 3 qualified opps within 30 days or shift forecast down
🟡 [3] Engineering: tech debt at 30% of sprint — shipping will slow by Q3
Action: CTO to propose debt sprint plan; COO to protect capacity
WATCH
→ People → Engineering cascade risk if attrition continues (see dimension interactions)
```
---
## Graceful Degradation
You don't need all metrics to run a diagnostic. The tool handles partial data:
- Missing metric → excluded from score, flagged as "[data needed]"
- Score still valid for available dimensions
- Report flags which gaps to fill for next cycle
## References
- `references/health-benchmarks.md` — benchmarks by stage (Seed, A, B, C)
- `scripts/health_scorer.py` — CLI scoring tool with traffic light output
FILE:org-health-diagnostic/references/health-benchmarks.md
# Org Health Benchmarks by Stage
Benchmarks for scoring each dimension at Seed, Series A, Series B, and Series C.
---
## Financial Health Benchmarks (CFO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| Runway (green) | >18mo | >12mo | >12mo | >18mo |
| Runway (yellow) | 9-18mo | 6-12mo | 6-12mo | 9-18mo |
| Runway (red) | <9mo | <6mo | <6mo | <9mo |
| Burn multiple (green) | <3x | <2x | <1.5x | <1x |
| Burn multiple (yellow) | 3-5x | 2-3x | 1.5-2.5x | 1-1.5x |
| Gross margin (green) | >50% | >65% | >70% | >75% |
| MoM growth (green) | >15% | >10% | >7% | >5% |
| Revenue concentration | <30% | <25% | <15% | <10% |
**Stage-specific notes:**
- **Seed:** Burn multiple is looser — you're investing in PMF, not efficiency
- **Series A:** Efficiency starts to matter; board watching burn multiple closely
- **Series B:** Capital efficiency is table stakes; burn >2x raises serious questions
- **Series C:** Approaching path to profitability; investors expect <1.5x
---
## Revenue Health Benchmarks (CRO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| NRR (green) | >100% | >110% | >115% | >120% |
| NRR (yellow) | 90-100% | 100-110% | 105-115% | 110-120% |
| NRR (red) | <90% | <100% | <105% | <110% |
| Logo churn (green) | <15%/yr | <10%/yr | <7%/yr | <5%/yr |
| Pipeline coverage | >2x | >3x | >3.5x | >4x |
| CAC payback (green) | <24mo | <18mo | <12mo | <9mo |
| Win rate (green) | >20% | >25% | >28% | >30% |
| ACV trend | growing | growing | growing | growing |
**What "green" NRR signals:**
- >100%: product creates value; expansion outpaces churn
- >110%: customers grow inside your platform; land-and-expand working
- >120%: exceptional — net negative churn; growth from existing base alone
- <100%: customers leave faster than others expand; structural retention problem
**Warning: NRR can mask problems.** NRR of 110% with 25% logo churn means you're retaining revenue from large customers while losing small ones. Check both.
---
## Product Health Benchmarks (CPO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| NPS (green) | >30 | >40 | >45 | >50 |
| NPS (yellow) | 10-30 | 20-40 | 30-45 | 40-50 |
| NPS (red) | <10 | <20 | <30 | <40 |
| DAU/MAU (green) | >25% | >35% | >40% | >45% |
| Core feature adoption | >40% | >55% | >65% | >70% |
| Time-to-value | <7 days | <5 days | <3 days | <2 days |
| CSAT | >4.0/5 | >4.2/5 | >4.3/5 | >4.4/5 |
**PMF proxy metrics:**
- "Very disappointed" if product disappeared: >40% = strong PMF signal (Sean Ellis test)
- 6-month retention cohort: >40% is healthy; <20% means PMF not yet achieved
- Organic referral rate: >20% of new users from referrals = product-led growth signal
**What low DAU/MAU actually means:**
- <20% DAU/MAU for a daily-use product = product isn't integrated into workflow
- DAU/MAU benchmarks vary by use case: email tool (daily use expected) vs. annual budget tool (weekly use is fine)
- Always compare to category, not absolute benchmarks
---
## Engineering Health Benchmarks (CTO)
DORA metrics are the industry standard (Google's DevOps Research and Assessment):
| Metric | Elite | High | Medium | Low |
|--------|-------|------|--------|-----|
| Deployment frequency | Multiple/day | Weekly | Monthly | <Monthly |
| Lead time for changes | <1 hour | 1 day-1 week | 1-6 months | >6 months |
| Change failure rate | <5% | 5-10% | 10-15% | >15% |
| MTTR | <1 hour | <1 day | 1 day-1 week | >1 week |
**Translation for startup stages:**
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| Deploy freq (green) | Weekly | Daily | Daily | Multiple/day |
| MTTR (green) | <4h | <2h | <1h | <30min |
| Change failure rate (green) | <15% | <10% | <7% | <5% |
| Tech debt ratio (green) | <30% | <25% | <20% | <15% |
| P0 incidents/month (green) | <3 | <2 | <2 | <1 |
**Warning signs unique to early-stage:**
- Bus factor = 1 on critical systems (one person knows how it works) → immediate risk
- No on-call rotation → incidents wake the same person every time → attrition risk
- No staging environment → production is the test environment → change failure spike risk
- "We'll fix it after launch" for >12 months → tech debt is now a strategic problem
---
## People Health Benchmarks (CHRO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| Regrettable attrition (green) | <15% | <12% | <10% | <8% |
| Regrettable attrition (red) | >25% | >18% | >15% | >12% |
| eNPS (green) | >20 | >30 | >35 | >40 |
| Time-to-fill (green) | <60d | <45d | <45d | <30d |
| Internal promotion rate | >20% | >25% | >30% | >35% |
| Manager span of control | 1:4-8 | 1:5-8 | 1:6-10 | 1:6-12 |
| % under-performers managed out | 3-5% | 3-5% | 3-5% | 3-5% |
**Regrettable vs non-regrettable attrition:**
- Regrettable: you'd rehire them immediately; they leave for better opportunity
- Non-regrettable: performance-based exits; mutual agreement; role evolution
- Only regrettable attrition signals health problems
**eNPS benchmarks by sector:**
- Tech startups: >30 is good; >50 is exceptional
- General: >0 means more promoters than detractors (minimum bar)
- Below -10: serious cultural issue; expect more attrition
**The cascade warning:** People health is a leading indicator, not lagging. By the time attrition shows up in your numbers, the next wave is already decided. Watch eNPS and engagement quarterly.
---
## Operational Health Benchmarks (COO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| OKR completion rate (green) | >60% | >70% | >75% | >80% |
| Decision cycle time (green) | <3 days | <2 days | <48h | <24h |
| Process maturity level | 1-2 | 2-3 | 3-4 | 4-5 |
| Cross-functional delivery (on time) | >60% | >70% | >75% | >80% |
| Leadership team tenure | N/A | >12mo avg | >18mo avg | >24mo avg |
**OKR interpretation:**
- 100% completion = OKRs were too easy (not ambitious enough)
- 60-70% completion = appropriate stretch, realistic execution
- <40% completion = disconnect between strategy and capacity, or OKRs set without buy-in
- OKRs nobody can remember = OKRs that don't guide decisions = wasted exercise
---
## Security Health Benchmarks (CISO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| Security incidents (P1+) | 0-1/yr | 0/yr | 0/yr | 0/yr |
| Pen test cadence | Annual | Annual | Bi-annual | Bi-annual |
| SOC 2 Type II | Roadmap | In progress | Complete | Complete |
| ISO 27001 | — | Roadmap | In progress | Complete |
| Security training completion | >80% | >90% | >95% | >95% |
| Critical CVE patching SLA | <72h | <48h | <24h | <12h |
| MFA coverage | >80% | >95% | 100% | 100% |
| Employee background checks | Key roles | All | All | All |
**Stage-specific compliance priorities:**
- **Seed:** Basic hygiene (MFA, encryption, access control)
- **Series A:** SOC 2 Type I on roadmap; sales increasingly requiring it
- **Series B:** SOC 2 Type II complete; ISO 27001 if selling to enterprise/EU
- **Series C:** Full compliance stack; GDPR, HIPAA if applicable
---
## Market Health Benchmarks (CMO)
| Metric | Seed | Series A | Series B | Series C |
|--------|------|----------|----------|----------|
| CAC trend | Acceptable | Improving | Improving | Stable/improving |
| Organic % of pipeline | >30% | >40% | >50% | >60% |
| Win rate (green) | >20% | >25% | >27% | >30% |
| Competitive win rate | >40% | >45% | >50% | >55% |
| Brand awareness in ICP | Low OK | Growing | Recognized | Leader |
| Content-to-pipeline conversion | Tracked | >2% | >3% | >4% |
---
## How Dimensions Interact
Understanding interdependencies helps predict cascades before they happen:
```
People Health degrades
↓ (60-90 day lag)
Engineering Health degrades (velocity drops, debt rises)
↓ (30-60 day lag)
Product Health degrades (features slip, quality drops)
↓ (60-90 day lag)
Revenue Health degrades (churn rises, deals stall)
↓ (30-60 day lag)
Financial Health degrades (cash gap, runway shortens)
↓ (immediate)
People Health degrades further (hiring freeze, morale)
```
**The prevention prescription:**
- Fix People and Engineering problems first — they cascade to everything
- Financial problems require immediate response (no lag)
- Revenue problems are often symptoms of Product or People problems upstream
- Security problems can cascade fast (breach → customer churn → financial → people)
**Weighting by stage (for overall score):**
| Dimension | Seed | Series A | Series B | Series C |
|-----------|------|----------|----------|----------|
| Financial | 30% | 25% | 20% | 20% |
| Revenue | 20% | 25% | 25% | 25% |
| People | 20% | 15% | 15% | 15% |
| Product | 15% | 15% | 15% | 15% |
| Engineering | 10% | 10% | 10% | 10% |
| Operations | 5% | 5% | 8% | 8% |
| Market | — | 5% | 5% | 5% |
| Security | — | — | 2% | 2% |
FILE:org-health-diagnostic/scripts/health_scorer.py
#!/usr/bin/env python3
"""
Org Health Diagnostic — Multi-Dimension Health Scorer
Scores 8 organizational dimensions on 1-10 scale with traffic lights.
Stdlib only. Run with: python health_scorer.py
"""
import json
import sys
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
from enum import Enum
class Stage(Enum):
SEED = "seed"
SERIES_A = "series_a"
SERIES_B = "series_b"
SERIES_C = "series_c"
class Trend(Enum):
IMPROVING = "improving"
STABLE = "stable"
DECLINING = "declining"
UNKNOWN = "unknown"
class TrafficLight(Enum):
GREEN = "green"
YELLOW = "yellow"
RED = "red"
# Stage weights: how much each dimension contributes to overall score
STAGE_WEIGHTS = {
Stage.SEED: {
"financial": 0.30, "revenue": 0.20, "people": 0.20,
"product": 0.15, "engineering": 0.10, "operations": 0.05,
"market": 0.00, "security": 0.00
},
Stage.SERIES_A: {
"financial": 0.25, "revenue": 0.25, "people": 0.15,
"product": 0.15, "engineering": 0.10, "operations": 0.05,
"market": 0.05, "security": 0.00
},
Stage.SERIES_B: {
"financial": 0.20, "revenue": 0.25, "people": 0.15,
"product": 0.15, "engineering": 0.10, "operations": 0.08,
"market": 0.05, "security": 0.02
},
Stage.SERIES_C: {
"financial": 0.20, "revenue": 0.25, "people": 0.15,
"product": 0.15, "engineering": 0.10, "operations": 0.08,
"market": 0.05, "security": 0.02
},
}
@dataclass
class Metric:
name: str
value: Optional[float]
unit: str
green_threshold: float # value at or above this = green
red_threshold: float # value at or below this = red
higher_is_better: bool = True
def score(self) -> Optional[float]:
"""Score 1-10. Returns None if no value."""
if self.value is None:
return None
v = self.value
g = self.green_threshold
r = self.red_threshold
if self.higher_is_better:
if v >= g:
# Scale 7-10 based on how far above green
excess = min((v - g) / max(g * 0.3, 0.01), 1.0)
return 7.0 + (3.0 * excess)
elif v <= r:
# Scale 1-3 based on how far below red
deficit = min((r - v) / max(r * 0.5, 0.01), 1.0)
return max(1.0, 3.0 - (2.0 * deficit))
else:
# Between red and green → 4-6
if g == r:
return 5.0
position = (v - r) / (g - r)
return 4.0 + (2.0 * position)
else:
# Lower is better — invert
if v <= g:
excess = min((g - v) / max(g * 0.3, 0.01), 1.0)
return 7.0 + (3.0 * excess)
elif v >= r:
deficit = min((v - r) / max(r * 0.5, 0.01), 1.0)
return max(1.0, 3.0 - (2.0 * deficit))
else:
if g == r:
return 5.0
position = (r - v) / (r - g)
return 4.0 + (2.0 * position)
def traffic_light(self) -> Optional[TrafficLight]:
s = self.score()
if s is None:
return None
if s >= 7:
return TrafficLight.GREEN
elif s >= 4:
return TrafficLight.YELLOW
return TrafficLight.RED
@dataclass
class Dimension:
key: str
name: str
owner: str
emoji: str
metrics: List[Metric]
trend: Trend = Trend.UNKNOWN
notes: str = ""
def score(self) -> Optional[float]:
"""Average of available metric scores."""
scores = [m.score() for m in self.metrics if m.score() is not None]
if not scores:
return None
return round(sum(scores) / len(scores), 1)
def traffic_light(self) -> TrafficLight:
s = self.score()
if s is None:
return TrafficLight.YELLOW # Unknown = watch
if s >= 7:
return TrafficLight.GREEN
elif s >= 4:
return TrafficLight.YELLOW
return TrafficLight.RED
def coverage(self) -> float:
"""% of metrics with data."""
filled = sum(1 for m in self.metrics if m.value is not None)
return filled / len(self.metrics) if self.metrics else 0.0
def missing_metrics(self) -> List[str]:
return [m.name for m in self.metrics if m.value is None]
def build_financial_dimension(stage: Stage, **kwargs) -> Dimension:
# Thresholds vary by stage
runway_green = {Stage.SEED: 18, Stage.SERIES_A: 12, Stage.SERIES_B: 12, Stage.SERIES_C: 18}
runway_red = {Stage.SEED: 9, Stage.SERIES_A: 6, Stage.SERIES_B: 6, Stage.SERIES_C: 9}
burn_green = {Stage.SEED: 3.0, Stage.SERIES_A: 2.0, Stage.SERIES_B: 1.5, Stage.SERIES_C: 1.0}
burn_red = {Stage.SEED: 5.0, Stage.SERIES_A: 3.0, Stage.SERIES_B: 2.5, Stage.SERIES_C: 1.5}
return Dimension(
key="financial",
name="Financial Health",
owner="CFO",
emoji="💰",
metrics=[
Metric("Runway (months)", kwargs.get("runway"),
"months", runway_green[stage], runway_red[stage]),
Metric("Burn multiple", kwargs.get("burn_multiple"),
"x", burn_green[stage], burn_red[stage], higher_is_better=False),
Metric("Gross margin (%)", kwargs.get("gross_margin"),
"%", 70, 55),
Metric("MoM growth (%)", kwargs.get("mom_growth"),
"%", 10, 4),
Metric("Revenue concentration (%)", kwargs.get("revenue_concentration"),
"%", 15, 30, higher_is_better=False),
],
trend=kwargs.get("financial_trend", Trend.UNKNOWN),
)
def build_revenue_dimension(stage: Stage, **kwargs) -> Dimension:
nrr_green = {Stage.SEED: 100, Stage.SERIES_A: 110, Stage.SERIES_B: 115, Stage.SERIES_C: 120}
nrr_red = {Stage.SEED: 90, Stage.SERIES_A: 100, Stage.SERIES_B: 105, Stage.SERIES_C: 110}
return Dimension(
key="revenue",
name="Revenue Health",
owner="CRO",
emoji="📈",
metrics=[
Metric("NRR (%)", kwargs.get("nrr"),
"%", nrr_green[stage], nrr_red[stage]),
Metric("Logo churn (%/yr)", kwargs.get("logo_churn"),
"%/yr", 5, 15, higher_is_better=False),
Metric("Pipeline coverage", kwargs.get("pipeline_coverage"),
"x", 3.0, 1.5),
Metric("CAC payback (months)", kwargs.get("cac_payback"),
"months", 12, 24, higher_is_better=False),
Metric("Win rate (%)", kwargs.get("win_rate"),
"%", 25, 15),
],
trend=kwargs.get("revenue_trend", Trend.UNKNOWN),
)
def build_product_dimension(**kwargs) -> Dimension:
return Dimension(
key="product",
name="Product Health",
owner="CPO",
emoji="🚀",
metrics=[
Metric("NPS", kwargs.get("nps"), "score", 40, 20),
Metric("DAU/MAU (%)", kwargs.get("dau_mau"), "%", 35, 15),
Metric("Core feature adoption (%)", kwargs.get("feature_adoption"), "%", 60, 30),
Metric("CSAT", kwargs.get("csat"), "/5", 4.2, 3.5),
Metric("Time-to-value (days)", kwargs.get("ttv_days"), "days", 3, 14, higher_is_better=False),
],
trend=kwargs.get("product_trend", Trend.UNKNOWN),
)
def build_engineering_dimension(**kwargs) -> Dimension:
# Deploy frequency encoded: 5=multiple/day, 4=daily, 3=weekly, 2=monthly, 1=<monthly
return Dimension(
key="engineering",
name="Engineering Health",
owner="CTO",
emoji="⚙️",
metrics=[
Metric("Deploy frequency (1-5)", kwargs.get("deploy_freq"), "scale", 4, 2),
Metric("Change failure rate (%)", kwargs.get("change_failure_rate"), "%", 5, 15, higher_is_better=False),
Metric("MTTR (hours)", kwargs.get("mttr_hours"), "hours", 1, 4, higher_is_better=False),
Metric("Tech debt ratio (%)", kwargs.get("tech_debt_pct"), "%", 15, 35, higher_is_better=False),
Metric("P0/P1 incidents/month", kwargs.get("incidents_monthly"), "count", 1, 5, higher_is_better=False),
],
trend=kwargs.get("engineering_trend", Trend.UNKNOWN),
)
def build_people_dimension(stage: Stage, **kwargs) -> Dimension:
attrition_green = {Stage.SEED: 15, Stage.SERIES_A: 12, Stage.SERIES_B: 10, Stage.SERIES_C: 8}
attrition_red = {Stage.SEED: 25, Stage.SERIES_A: 18, Stage.SERIES_B: 15, Stage.SERIES_C: 12}
return Dimension(
key="people",
name="People Health",
owner="CHRO",
emoji="👥",
metrics=[
Metric("Regrettable attrition (%/yr)", kwargs.get("attrition"),
"%/yr", attrition_green[stage], attrition_red[stage], higher_is_better=False),
Metric("eNPS", kwargs.get("enps"), "score", 30, 0),
Metric("Time-to-fill (days)", kwargs.get("ttf_days"), "days", 45, 90, higher_is_better=False),
Metric("Internal promotion rate (%)", kwargs.get("internal_promo_rate"), "%", 25, 10),
],
trend=kwargs.get("people_trend", Trend.UNKNOWN),
)
def build_operations_dimension(**kwargs) -> Dimension:
return Dimension(
key="operations",
name="Operational Health",
owner="COO",
emoji="🔄",
metrics=[
Metric("OKR completion rate (%)", kwargs.get("okr_completion"), "%", 70, 50),
Metric("Decision cycle time (hours)", kwargs.get("decision_hours"), "hours", 48, 168, higher_is_better=False),
Metric("Process maturity (1-5)", kwargs.get("process_maturity"), "level", 3, 1.5),
Metric("Cross-functional delivery (%)", kwargs.get("xfn_delivery_rate"), "%", 70, 50),
],
trend=kwargs.get("ops_trend", Trend.UNKNOWN),
)
def build_security_dimension(**kwargs) -> Dimension:
return Dimension(
key="security",
name="Security Health",
owner="CISO",
emoji="🔒",
metrics=[
Metric("Security incidents (90 days)", kwargs.get("incidents_90d"), "count", 0, 1, higher_is_better=False),
Metric("MFA coverage (%)", kwargs.get("mfa_coverage"), "%", 95, 80),
Metric("Security training completion (%)", kwargs.get("training_completion"), "%", 95, 80),
Metric("Critical CVE patch rate (%)", kwargs.get("cve_patch_rate"), "%", 100, 85),
Metric("Pen test recency (months)", kwargs.get("pentest_months"), "months", 12, 24, higher_is_better=False),
],
trend=kwargs.get("security_trend", Trend.UNKNOWN),
)
def build_market_dimension(**kwargs) -> Dimension:
return Dimension(
key="market",
name="Market Health",
owner="CMO",
emoji="📣",
metrics=[
Metric("Organic pipeline % ", kwargs.get("organic_pipeline_pct"), "%", 40, 20),
Metric("Competitive win rate (%)", kwargs.get("competitive_win_rate"), "%", 45, 30),
Metric("CAC trend (1=worsening, 5=improving)", kwargs.get("cac_trend_score"), "scale", 4, 2),
],
trend=kwargs.get("market_trend", Trend.UNKNOWN),
)
def calculate_overall(dimensions: List[Dimension], stage: Stage) -> Optional[float]:
weights = STAGE_WEIGHTS[stage]
total_weight = 0.0
weighted_sum = 0.0
for dim in dimensions:
score = dim.score()
w = weights.get(dim.key, 0.0)
if score is not None and w > 0:
weighted_sum += score * w
total_weight += w
if total_weight == 0:
return None
return round(weighted_sum / total_weight, 1)
def trend_arrow(trend: Trend) -> str:
return {
Trend.IMPROVING: "↑",
Trend.STABLE: "→",
Trend.DECLINING: "↓",
Trend.UNKNOWN: "?",
}[trend]
def traffic_light_icon(tl: TrafficLight) -> str:
return {"green": "🟢", "yellow": "🟡", "red": "🔴"}[tl.value]
def print_dashboard(dimensions: List[Dimension], overall: Optional[float],
stage: Stage, company: str = "Company") -> None:
"""Print the full health dashboard."""
print("\n" + "=" * 65)
print(f"ORG HEALTH DIAGNOSTIC — {company.upper()}")
print(f"Stage: {stage.value.replace('_', ' ').title()}")
if overall is not None:
overall_tl = TrafficLight.GREEN if overall >= 7 else (TrafficLight.YELLOW if overall >= 4 else TrafficLight.RED)
print(f"Overall: {traffic_light_icon(overall_tl)} {overall}/10")
print("=" * 65)
print("\nDIMENSION SCORES")
print("─" * 65)
priority_reds = []
priority_yellows = []
for dim in dimensions:
score = dim.score()
tl = dim.traffic_light()
icon = traffic_light_icon(tl)
trend = trend_arrow(dim.trend)
coverage = int(dim.coverage() * 100)
score_str = f"{score:.1f}" if score is not None else "N/A"
cov_str = f"({coverage}% data)" if coverage < 100 else ""
print(f"{dim.emoji} {dim.name:<22} {icon} {score_str:<5} {trend} {dim.owner} {cov_str}")
if tl == TrafficLight.RED and score is not None:
priority_reds.append(dim)
elif tl == TrafficLight.YELLOW and score is not None:
priority_yellows.append(dim)
# Top priorities
if priority_reds or priority_yellows:
print(f"\n{'─' * 65}")
print("PRIORITIES")
print("─" * 65)
idx = 1
for dim in priority_reds[:3]:
print(f"\n🔴 [{idx}] {dim.name} — Score: {dim.score():.1f}/10")
# Show worst metric
worst = min(
[m for m in dim.metrics if m.score() is not None],
key=lambda m: m.score(),
default=None
)
if worst:
print(f" Worst metric: {worst.name} = {worst.value}{worst.unit}")
missing = dim.missing_metrics()
if missing:
print(f" Missing data: {', '.join(missing)}")
idx += 1
for dim in priority_yellows[:2]:
print(f"\n🟡 [{idx}] {dim.name} — Score: {dim.score():.1f}/10 — {trend_arrow(dim.trend)}")
idx += 1
# Data gaps
all_missing = [(dim.name, dim.missing_metrics()) for dim in dimensions if dim.missing_metrics()]
if all_missing:
print(f"\n{'─' * 65}")
print("DATA GAPS (fill to improve diagnostic accuracy)")
for dim_name, metrics in all_missing:
print(f" {dim_name}: {', '.join(metrics)}")
# Cascade warnings
print(f"\n{'─' * 65}")
print("CASCADE RISK")
red_keys = {d.key for d in dimensions if d.traffic_light() == TrafficLight.RED}
if "people" in red_keys:
print(" ⚠️ People RED → Engineering velocity drop expected in 60-90 days")
if "engineering" in red_keys:
print(" ⚠️ Engineering RED → Product quality at risk; roadmap will slip")
if "product" in red_keys:
print(" ⚠️ Product RED → Revenue retention at risk within 2 quarters")
if "revenue" in red_keys:
print(" ⚠️ Revenue RED → Financial pressure mounting; watch runway")
if "financial" in red_keys:
print(" 🚨 Financial RED → All dimensions at risk; immediate board action needed")
if not red_keys:
print(" ✅ No active cascade risks detected")
print(f"\n{'=' * 65}\n")
def to_json(dimensions: List[Dimension], overall: Optional[float], stage: Stage) -> Dict:
result = {
"stage": stage.value,
"overall_score": overall,
"overall_traffic_light": (
TrafficLight.GREEN if overall and overall >= 7
else TrafficLight.YELLOW if overall and overall >= 4
else TrafficLight.RED
).value if overall else "unknown",
"dimensions": {}
}
for dim in dimensions:
result["dimensions"][dim.key] = {
"name": dim.name,
"owner": dim.owner,
"score": dim.score(),
"traffic_light": dim.traffic_light().value,
"trend": dim.trend.value,
"coverage_pct": round(dim.coverage() * 100),
"missing_metrics": dim.missing_metrics(),
"metrics": [
{
"name": m.name,
"value": m.value,
"unit": m.unit,
"score": m.score(),
"traffic_light": m.traffic_light().value if m.traffic_light() else None,
}
for m in dim.metrics
]
}
return result
def build_sample_data(stage: Stage) -> Dict:
"""Sample Series A company data."""
return dict(
# Financial
runway=14, burn_multiple=1.8, gross_margin=68, mom_growth=8.5,
revenue_concentration=28, financial_trend=Trend.STABLE,
# Revenue
nrr=104, logo_churn=8, pipeline_coverage=1.9, cac_payback=16,
win_rate=22, revenue_trend=Trend.DECLINING,
# Product
nps=38, dau_mau=32, feature_adoption=52, csat=4.1,
ttv_days=6, product_trend=Trend.STABLE,
# Engineering
deploy_freq=3, change_failure_rate=9, mttr_hours=2.8,
tech_debt_pct=30, incidents_monthly=2, engineering_trend=Trend.STABLE,
# People
attrition=21, enps=12, ttf_days=58, internal_promo_rate=18,
people_trend=Trend.DECLINING,
# Operations
okr_completion=62, decision_hours=72, process_maturity=2.5,
xfn_delivery_rate=65, ops_trend=Trend.STABLE,
# Security
incidents_90d=0, mfa_coverage=88, training_completion=82,
cve_patch_rate=95, pentest_months=14, security_trend=Trend.IMPROVING,
# Market
organic_pipeline_pct=35, competitive_win_rate=42,
cac_trend_score=3, market_trend=Trend.STABLE,
)
def interactive_mode(stage: Stage) -> Dict:
"""Guided metric entry."""
print("\nEnter metrics (press Enter to skip):\n")
data = {}
def ask(prompt: str, key: str, default=None):
val = input(f" {prompt}: ").strip()
if val:
try:
data[key] = float(val)
except ValueError:
pass
print("💰 FINANCIAL")
ask("Runway (months)", "runway")
ask("Burn multiple (e.g. 1.8)", "burn_multiple")
ask("Gross margin (%)", "gross_margin")
ask("MoM growth (%)", "mom_growth")
ask("Top customer % of ARR", "revenue_concentration")
print("\n📈 REVENUE")
ask("NRR (%)", "nrr")
ask("Logo churn (%/yr)", "logo_churn")
ask("Pipeline coverage (x)", "pipeline_coverage")
ask("CAC payback (months)", "cac_payback")
ask("Win rate (%)", "win_rate")
print("\n🚀 PRODUCT")
ask("NPS score", "nps")
ask("DAU/MAU (%)", "dau_mau")
ask("Core feature adoption (%)", "feature_adoption")
print("\n⚙️ ENGINEERING")
ask("Deploy frequency (1=rare, 5=multiple/day)", "deploy_freq")
ask("Change failure rate (%)", "change_failure_rate")
ask("MTTR (hours)", "mttr_hours")
ask("Tech debt % of sprint", "tech_debt_pct")
print("\n👥 PEOPLE")
ask("Regrettable attrition (%/yr)", "attrition")
ask("eNPS score", "enps")
ask("Time-to-fill (days)", "ttf_days")
print("\n🔄 OPERATIONS")
ask("OKR completion rate (%)", "okr_completion")
print("\n🔒 SECURITY")
ask("MFA coverage (%)", "mfa_coverage")
ask("Security training completion (%)", "training_completion")
return data
def main():
print("\n🏥 ORG HEALTH DIAGNOSTIC")
print("Multi-dimension organizational health scorer\n")
# Determine stage
stage_map = {
"seed": Stage.SEED, "a": Stage.SERIES_A, "series_a": Stage.SERIES_A,
"b": Stage.SERIES_B, "series_b": Stage.SERIES_B,
"c": Stage.SERIES_C, "series_c": Stage.SERIES_C,
}
stage_arg = next((a for a in sys.argv[1:] if a.lower() in stage_map), None)
stage = stage_map.get(stage_arg.lower(), Stage.SERIES_A) if stage_arg else Stage.SERIES_A
if "--interactive" in sys.argv or "-i" in sys.argv:
company = input("Company name: ").strip() or "Company"
stage_input = input("Stage (seed/a/b/c): ").strip().lower()
stage = stage_map.get(stage_input, Stage.SERIES_A)
data = interactive_mode(stage)
else:
print(f"Running sample Series A company data.")
print("(Use --interactive or -i for custom data, --stage seed/a/b/c for stage)\n")
company = "Sample Co"
data = build_sample_data(stage)
# Build dimensions
dimensions = [
build_financial_dimension(stage, **data),
build_revenue_dimension(stage, **data),
build_product_dimension(**data),
build_engineering_dimension(**data),
build_people_dimension(stage, **data),
build_operations_dimension(**data),
build_security_dimension(**data),
build_market_dimension(**data),
]
overall = calculate_overall(dimensions, stage)
print_dashboard(dimensions, overall, stage, company)
if "--json" in sys.argv:
print(json.dumps(to_json(dimensions, overall, stage), indent=2))
if __name__ == "__main__":
main()
FILE:scenario-war-room/SKILL.md
---
name: "scenario-war-room"
description: "Cross-functional what-if modeling for cascading multi-variable scenarios. Unlike single-assumption stress testing, this models compound adversity across all business functions simultaneously. Use when facing complex risk scenarios, strategic decisions with major downside, or when the user asks 'what if X AND Y both happen?'"
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: strategic-planning
updated: 2026-03-05
python-tools: scenario_modeler.py
frameworks: scenario-planning
---
# Scenario War Room
Model cascading what-if scenarios across all business functions. Not single-assumption stress tests — compound adversity that shows how one problem creates the next.
## Keywords
scenario planning, war room, what-if analysis, risk modeling, cascading effects, compound risk, adversity planning, contingency planning, stress test, crisis planning, multi-variable scenario, pre-mortem
## Quick Start
```bash
python scripts/scenario_modeler.py # Interactive scenario builder with cascade modeling
```
Or describe the scenario:
```
/war-room "What if we lose our top customer AND miss the Q3 fundraise?"
/war-room "What if 3 engineers quit AND we need to ship by Q3?"
/war-room "What if our market shrinks 30% AND a competitor raises $50M?"
```
## What This Is Not
- **Not** a single-assumption stress test (that's `/em:stress-test`)
- **Not** financial modeling only — every function gets modeled
- **Not** worst-case-only — models 3 severity levels
- **Not** paralysis by analysis — outputs concrete hedges and triggers
## Framework: 6-Step Cascade Model
### Step 1: Define Scenario Variables (max 3)
State each variable with:
- **What changes** — specific, quantified if possible
- **Probability** — your best estimate
- **Timeline** — when it hits
```
Variable A: Top customer (28% ARR) gives 60-day termination notice
Probability: 15% | Timeline: Within 90 days
Variable B: Series A fundraise delayed 6 months beyond target close
Probability: 25% | Timeline: Q3
Variable C: Lead engineer resigns
Probability: 20% | Timeline: Unknown
```
### Step 2: Domain Impact Mapping
For each variable, each relevant role models impact:
| Domain | Owner | Models |
|--------|-------|--------|
| Cash & runway | CFO | Burn impact, runway change, bridge options |
| Revenue | CRO | ARR gap, churn cascade risk, pipeline |
| Product | CPO | Roadmap impact, PMF risk |
| Engineering | CTO | Velocity impact, key person risk |
| People | CHRO | Attrition cascade, hiring freeze implications |
| Operations | COO | Capacity, OKR impact, process risk |
| Security | CISO | Compliance timeline risk |
| Market | CMO | CAC impact, competitive exposure |
### Step 3: Cascade Effect Mapping
This is the core. Show how Variable A triggers consequences in domains that trigger Variable B's effects:
```
TRIGGER: Customer churn ($560K ARR)
↓
CFO: Runway drops 14 → 8 months
↓
CHRO: Hiring freeze; retention risk increases (morale hit)
↓
CTO: 3 open engineering reqs frozen; roadmap slips
↓
CPO: Q4 feature launch delayed → customer retention risk
↓
CRO: NRR drops; existing accounts see reduced velocity → more churn risk
↓
CFO: [Secondary cascade — potential death spiral if not interrupted]
```
Name the cascade explicitly. Show where it can be interrupted.
### Step 4: Severity Matrix
Model three scenarios:
| Scenario | Definition | Recovery |
|----------|------------|---------|
| **Base** | One variable hits; others don't | Manageable with plan |
| **Stress** | Two variables hit simultaneously | Requires significant response |
| **Severe** | All variables hit; full cascade | Existential; requires board intervention |
For each severity level:
- Runway impact
- ARR impact
- Headcount impact
- Timeline to unacceptable state (trigger point)
### Step 5: Trigger Points (Early Warning Signals)
Define the measurable signal that tells you a scenario is unfolding **before** it's confirmed:
```
Trigger for Customer Churn Risk:
- Sponsor goes dark for >3 weeks
- Usage drops >25% MoM
- No Q1 QBR confirmed by Dec 1
Trigger for Fundraise Delay:
- <3 term sheets after 60 days of process
- Lead investor requests >30-day extension on DD
- Competitor raises at lower valuation (market signal)
Trigger for Engineering Attrition:
- Glassdoor activity from engineering team
- 2+ referral interview requests from engineers
- Above-market offer counter-required in last 3 months
```
### Step 6: Hedging Strategies
For each scenario: actions to take **now** (before the scenario materializes) that reduce impact if it does.
| Hedge | Cost | Impact | Owner | Deadline |
|-------|------|--------|-------|---------|
| Establish $500K credit line | $5K/year | Buys 3 months if churn hits | CFO | 60 days |
| 12-month retention bonus for 3 key engineers | $90K | Locks team through fundraise | CHRO | 30 days |
| Diversify to <20% revenue concentration per customer | Sales effort | Reduces single-customer risk | CRO | 2 quarters |
| Compress fundraise timeline, start parallel process | CEO time | Closes before runways merge | CEO | Immediate |
---
## Output Format
Every war room session produces:
```
SCENARIO: [Name]
Variables: [A, B, C]
Most likely path: [which combination actually plays out, with probability]
SEVERITY LEVELS
Base (A only): [runway/ARR impact] — recovery: [X actions]
Stress (A+B): [runway/ARR impact] — recovery: [X actions]
Severe (A+B+C): [runway/ARR impact] — existential risk: [yes/no]
CASCADE MAP
[A → domain impact → B trigger → domain impact → end state]
EARLY WARNING SIGNALS
- [Signal 1 → which scenario it indicates]
- [Signal 2 → which scenario it indicates]
- [Signal 3 → which scenario it indicates]
HEDGES (take these actions now)
1. [Action] — cost: $X — impact: [what it buys] — owner: [role] — deadline: [date]
2. [Action] — cost: $X — impact: [what it buys] — owner: [role] — deadline: [date]
3. [Action] — cost: $X — impact: [what it buys] — owner: [role] — deadline: [date]
RECOMMENDED DECISION
[One paragraph. What to do, in what order, and why.]
```
---
## Rules for Good War Room Sessions
**Max 3 variables per scenario.** More than 3 is noise — you can't meaningfully prepare for 5-variable collapse. Model the 3 that actually worry you.
**Quantify or estimate.** "Revenue drops" is not useful. "$420K ARR at risk over 60 days" is. Use ranges if uncertain.
**Don't stop at first-order effects.** The damage is always in the cascade, not the initial hit.
**Model recovery, not just impact.** Every scenario should have a "what we do" path.
**Separate base case from sensitivity.** Don't conflate "what probably happens" with "what could happen."
**Don't over-model.** 3-4 scenarios per planning cycle is the right number. More creates analysis paralysis.
---
## Common Scenarios by Stage
**Seed:**
- Co-founder leaves + product misses launch
- Funding runs out + bridge terms unfavorable
**Series A:**
- Miss ARR target + fundraise delayed
- Key customer churns + competitor raises
**Series B:**
- Market contraction + burn multiple spikes
- Lead investor wants pivot + team resists
## Integration with C-Suite Roles
| Scenario Type | Primary Roles | Cascade To |
|--------------|---------------|------------|
| Revenue miss | CRO, CFO | CMO (pipeline), COO (cuts), CHRO (layoffs) |
| Key person departure | CHRO, COO | CTO (if eng), CRO (if sales) |
| Fundraise failure | CFO, CEO | COO (runway extension), CHRO (hiring freeze) |
| Security breach | CISO, CTO | CEO (comms), CFO (cost), CRO (customer impact) |
| Market shift | CEO, CPO | CMO (repositioning), CRO (new segments) |
| Competitor move | CMO, CRO | CPO (roadmap response), CEO (strategy) |
## References
- `references/scenario-planning.md` — Shell methodology, pre-mortem, Monte Carlo, cascade frameworks
- `scripts/scenario_modeler.py` — CLI tool for structured scenario modeling
FILE:scenario-war-room/references/scenario-planning.md
# Scenario Planning Reference
## Shell's Scenario Planning Methodology
Shell invented modern scenario planning in the 1970s after the oil crisis. Core insight: **scenarios are not forecasts — they're tools for thinking.**
### Shell's Principles (adapted for startups)
1. **Scenarios are mutually exclusive, collectively exhaustive** — they cover the space of possibilities without overlapping
2. **2x2 matrix** — pick 2 critical uncertainties (not risks — uncertainties); cross them to get 4 scenarios
3. **Name the scenarios** — named scenarios are remembered; numbered ones aren't
4. **Identify predetermined elements** — things that will happen regardless of scenario (regulatory changes, tech trends)
5. **Early indicators** — each scenario has signals you can monitor today
### Shell's 2x2 for Startups
Critical uncertainties for early-stage SaaS:
| | Market grows fast | Market grows slow |
|---|---|---|
| **We raise successfully** | "Blue Ocean" — execute hard | "Ramp Carefully" — efficiency focus |
| **We bridge/delay raise** | "Scrappy Growth" — ramen profitability | "Survival Mode" — cut to core |
Build your war room sessions around whichever quadrant is most relevant right now.
---
## Monte Carlo Thinking for Startups
Monte Carlo = running thousands of simulations with random variables to understand probability distributions.
You don't need software. Apply the mental model:
### The Mental Monte Carlo Process
1. **Identify the key variables** (3-5 max)
2. **Assign ranges** — not point estimates
- CAC: $6K–$12K (uniform distribution)
- Close rate: 20%–40% (normal, mean 30%)
- Churn: 5%–20% (right-skewed — bad tail is worse)
3. **Run mental scenarios** — pick low/mid/high for each
4. **Identify the combinations that kill you** — which variable combinations make runway hit zero?
5. **Focus hedging on** the 20% of combinations that account for 80% of kill scenarios
### Practical Monte Carlo Heuristic
For revenue forecasting, always state:
- **P90** (90% confidence you'll exceed this)
- **P50** (median case)
- **P10** (only 10% chance you'll exceed this — your "stretch")
Boards respect ranges. Point estimates are usually wrong and make you look naive.
---
## Pre-Mortem Technique
A pre-mortem asks: *"It's 12 months from now. We failed. Why?"*
It's the opposite of planning (which asks why you'll succeed). It surfaces hidden risks that optimism suppresses.
### Running a Pre-Mortem
**Setup:**
- Time: 90 minutes
- Participants: leadership team
- Facilitator: neutral (COO, or external)
- Assumption: "It's [date 12 months out]. The company failed / missed its major goal. This is real."
**Phase 1 — Silence (10 minutes):**
Each person writes their top 3 reasons the failure happened. No discussion.
**Phase 2 — Round Robin (30 minutes):**
Each person shares one reason per turn. Facilitator captures on whiteboard. No debate yet.
**Phase 3 — Cluster (20 minutes):**
Group similar causes. Identify the top 5 clusters.
**Phase 4 — Probability & Impact (20 minutes):**
For each cluster: P(likely) × impact = risk score. Rank.
**Phase 5 — Mitigation (10 minutes):**
Top 3 risks: what one action would most reduce each?
### Pre-Mortem Prompt Variants
- "It's March 2027. We ran out of money. Why?"
- "It's Q4. We lost 3 enterprise customers in 60 days. What happened?"
- "It's next year. Our top competitor took 40% of the market. How?"
- "It's 18 months from now. Half the engineering team left. What triggered it?"
---
## Cascade Effect Mapping
Cascades are where most startups get surprised. The first hit is expected — the second and third aren't.
### Cascade Mapping Format
Draw as a chain:
```
INITIAL EVENT
↓ [immediate effect: domain, severity, timeline]
SECONDARY EFFECT
↓ [cascade mechanism: how A causes B]
TERTIARY EFFECT
↓ [cascade mechanism]
END STATE [runway impact, ARR impact, team impact]
```
### Common Cascade Patterns
**Revenue → Cash → People:**
```
Customer churns ($400K ARR)
↓ CFO: runway drops 14→9 months; bridge needed
↓ CHRO: hiring freeze; morale drops; attrition risk
↓ CTO: roadmap slips; key engineers leave for certainty
↓ CPO: product quality drops; more churn risk
↓ CRO: harder to win new logos without product velocity
END STATE: Death spiral if not interrupted at step 2
```
**Fundraise → Operations → Product:**
```
Fundraise delayed 6 months
↓ CFO: bridge at unfavorable terms; equity dilution
↓ COO: freeze all non-essential spend; process degrades
↓ CPO: roadmap cut to 40% of planned scope
↓ CTO: no infra investment; tech debt accelerates
↓ CRO: product gaps start losing deals to feature-complete competitors
END STATE: Weaker position at next raise; lower valuation
```
**People → Product → Revenue:**
```
Lead engineer + 2 seniors leave (30% of eng team)
↓ CTO: velocity drops 50%; critical features slip Q3→Q4
↓ CPO: Q4 launch cancelled; roadmap confidence collapses
↓ CRO: 3 enterprise deals cite product timeline → delays/losses
↓ CFO: $600K pipeline at risk; raises needed earlier
END STATE: Fundraise from position of weakness; team morale spiral
```
### Identifying Cascade Break Points
Every cascade has a point where intervention is cheapest. Find it:
- Step 1: Very expensive to prevent (existential)
- Step 2: Moderate cost (management action)
- Step 3: Cheap (early signal response)
Always try to interrupt at Step 2 or earlier.
---
## Trigger-Based Contingency Plans
Triggers are measurable signals you commit to acting on **before** the scenario fully materializes.
### Trigger Design Principles
1. **Measurable** — not "things look bad" but "cash below $800K"
2. **Leading, not lagging** — triggers should fire 60-90 days before the crisis
3. **Pre-committed responses** — when trigger fires, the action is already decided
4. **Owner assigned** — who watches for this trigger?
### Trigger Examples
**Cash / Runway:**
```
Trigger: Cash drops below $1M (or runway < 6 months)
Pre-committed response:
- CFO: activate credit line within 48 hours
- CEO: begin bridge conversations with existing investors
- COO: implement 20% spend reduction plan (already drafted)
Owner: CFO (weekly cash report to CEO)
```
**Customer Health:**
```
Trigger: Any customer >10% ARR shows 3 of: [sponsor gone dark, usage -25%,
no renewal discussion by 90 days before contract end, missed QBR]
Pre-committed response:
- CRO: executive escalation call within 48 hours
- CPO: product health review scheduled
- CEO: direct outreach if escalation fails
Owner: CRO (health score dashboard, weekly)
```
**Fundraise:**
```
Trigger: <3 term sheets after 8 weeks of active process
Pre-committed response:
- CEO: expand process to 10 additional firms
- CFO: model bridge scenarios; draft bridge terms
- COO: prepare 90-day cost reduction plan
Owner: CEO (weekly fundraise status)
```
---
## How Many Scenarios to Model
**Answer: 3-4 max per planning cycle.**
The math: 3 scenarios × 6 domains × 3 severity levels = 54 combinations. That's already overwhelming. More scenarios don't improve decisions — they paralyze them.
### The Right 3-4 Scenarios
1. **Most likely adverse scenario** — what actually keeps you up at night
2. **Market/macro scenario** — something outside your control
3. **Black swan** — low probability, existential if it hits
4. **Compound scenario** — your top 2 adverse events happening simultaneously
### What Kills Scenario Planning
- **Too many scenarios** — decision paralysis
- **Only modeling what's comfortable** — survivorship bias
- **No pre-committed responses** — it's just worry, not planning
- **Not revisiting** — scenarios from 12 months ago are often irrelevant
- **Treating scenarios as forecasts** — they're possibilities, not predictions
- **Confusing risk with uncertainty** — risk has known probabilities; uncertainty doesn't
FILE:scenario-war-room/scripts/scenario_modeler.py
#!/usr/bin/env python3
"""
Scenario War Room — Multi-Variable Cascade Modeler
Models cascading effects of compound adversity across business domains.
Stdlib only. Run with: python scenario_modeler.py
"""
import json
import sys
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
from enum import Enum
class Severity(Enum):
BASE = "base" # One variable hits
STRESS = "stress" # Two variables hit
SEVERE = "severe" # All variables hit
class Domain(Enum):
FINANCIAL = "Financial (CFO)"
REVENUE = "Revenue (CRO)"
PRODUCT = "Product (CPO)"
ENGINEERING = "Engineering (CTO)"
PEOPLE = "People (CHRO)"
OPERATIONS = "Operations (COO)"
SECURITY = "Security (CISO)"
MARKET = "Market (CMO)"
@dataclass
class Variable:
name: str
description: str
probability: float # 0.0-1.0
arrt_impact_pct: float # % of ARR at risk (negative = loss)
runway_impact_months: float # months lost from runway (negative = reduction)
affected_domains: List[Domain]
timeline_days: int # when it hits
@dataclass
class CascadeEffect:
trigger_domain: Domain
caused_domain: Domain
mechanism: str # how A causes B
severity_multiplier: float # compounds the base impact
@dataclass
class Hedge:
action: str
cost_usd: int
impact_description: str
owner: str
deadline_days: int
reduces_probability: float # how much it reduces scenario probability
@dataclass
class Scenario:
name: str
variables: List[Variable]
cascades: List[CascadeEffect]
hedges: List[Hedge]
# Company baseline
current_arr_usd: int = 2_000_000
current_runway_months: int = 14
monthly_burn_usd: int = 140_000
def calculate_impact(
scenario: Scenario,
severity: Severity
) -> Dict:
"""Calculate combined impact for a given severity level."""
variables = scenario.variables
# Select variables by severity
if severity == Severity.BASE:
active_vars = variables[:1]
elif severity == Severity.STRESS:
active_vars = variables[:2]
else:
active_vars = variables
# Direct impacts
total_arr_loss_pct = sum(abs(v.arrt_impact_pct) for v in active_vars)
total_runway_reduction = sum(abs(v.runway_impact_months) for v in active_vars)
arr_at_risk = scenario.current_arr_usd * (total_arr_loss_pct / 100)
new_arr = scenario.current_arr_usd - arr_at_risk
new_runway = scenario.current_runway_months - total_runway_reduction
# Cascade multiplier (stress/severe amplify via domain cascades)
cascade_multiplier = 1.0
if len(active_vars) > 1:
active_domains = set(d for v in active_vars for d in v.affected_domains)
for cascade in scenario.cascades:
if (cascade.trigger_domain in active_domains and
cascade.caused_domain in active_domains):
cascade_multiplier *= cascade.severity_multiplier
# Apply cascade
effective_arr_loss = arr_at_risk * cascade_multiplier
effective_arr = scenario.current_arr_usd - effective_arr_loss
effective_runway = max(0, new_runway - (cascade_multiplier - 1.0) * 2)
# New burn multiple
new_monthly_burn = scenario.monthly_burn_usd * cascade_multiplier
burn_multiple = (new_monthly_burn * 12) / max(effective_arr, 1)
# Affected domains
affected = set(d for v in active_vars for d in v.affected_domains)
return {
"severity": severity.value,
"active_variables": [v.name for v in active_vars],
"arr_at_risk_usd": int(effective_arr_loss),
"arr_at_risk_pct": round(effective_arr_loss / scenario.current_arr_usd * 100, 1),
"projected_arr_usd": int(effective_arr),
"runway_months": round(effective_runway, 1),
"runway_change": round(effective_runway - scenario.current_runway_months, 1),
"cascade_multiplier": round(cascade_multiplier, 2),
"new_burn_multiple": round(burn_multiple, 1),
"affected_domains": [d.value for d in affected],
"existential_risk": effective_runway < 6.0,
"board_escalation_required": effective_runway < 9.0,
}
def identify_triggers(variables: List[Variable]) -> List[Dict]:
"""Generate early warning triggers for each variable."""
triggers = []
for var in variables:
trigger = {
"variable": var.name,
"timeline": f"Watch from day 1; expect signal ~{var.timeline_days // 2} days before impact",
"signals": _generate_signals(var),
"response_owner": _domain_to_owner(var.affected_domains[0] if var.affected_domains else Domain.FINANCIAL),
}
triggers.append(trigger)
return triggers
def _generate_signals(var: Variable) -> List[str]:
"""Generate plausible early warning signals based on variable type."""
signals = []
name_lower = var.name.lower()
if any(k in name_lower for k in ["customer", "churn", "account"]):
signals = [
"Executive sponsor unreachable for >2 weeks",
"Product usage drops >20% month-over-month",
"No QBR scheduled within 90 days of contract renewal",
"Support ticket volume spikes >50% without explanation",
]
elif any(k in name_lower for k in ["fundraise", "raise", "capital", "investor"]):
signals = [
"Fewer than 3 term sheets after 60 days of active process",
"Lead investor requests 30+ day extension on diligence",
"Comparable company raises at lower valuation (market signal)",
"Investor meeting conversion rate below 20%",
]
elif any(k in name_lower for k in ["engineer", "people", "team", "resign", "quit"]):
signals = [
"2+ engineers receive above-market counter-offer in 90 days",
"Glassdoor activity increases from engineering team",
"Key person requests 1:1 to 'talk about career' unexpectedly",
"Referral interview requests from engineers increase",
]
elif any(k in name_lower for k in ["market", "competitor", "competition"]):
signals = [
"Competitor raises $10M+ funding round",
"Win/loss rate shifts >10% in 60 days",
"Multiple prospects cite competitor by name in objections",
"Competitor poaches 2+ of your customers in a quarter",
]
else:
signals = [
f"Leading indicator for '{var.name}' deteriorates 20%+ vs baseline",
"Weekly metric review shows 3-week trend in wrong direction",
"External validation from customers or partners confirms risk",
]
return signals[:3] # Top 3
def _domain_to_owner(domain: Domain) -> str:
mapping = {
Domain.FINANCIAL: "CFO",
Domain.REVENUE: "CRO",
Domain.PRODUCT: "CPO",
Domain.ENGINEERING: "CTO",
Domain.PEOPLE: "CHRO",
Domain.OPERATIONS: "COO",
Domain.SECURITY: "CISO",
Domain.MARKET: "CMO",
}
return mapping.get(domain, "CEO")
def format_currency(amount: int) -> str:
if amount >= 1_000_000:
return f".1fM"
elif amount >= 1_000:
return f".0fK"
return f"amount"
def print_report(scenario: Scenario) -> None:
"""Print full scenario analysis report."""
print("\n" + "=" * 70)
print(f"SCENARIO WAR ROOM: {scenario.name.upper()}")
print("=" * 70)
# Baseline
print(f"\n📊 BASELINE")
print(f" Current ARR: {format_currency(scenario.current_arr_usd)}")
print(f" Monthly Burn: {format_currency(scenario.monthly_burn_usd)}")
print(f" Runway: {scenario.current_runway_months} months")
# Variables
print(f"\n⚡ SCENARIO VARIABLES ({len(scenario.variables)})")
for i, var in enumerate(scenario.variables, 1):
prob_pct = int(var.probability * 100)
print(f"\n Variable {i}: {var.name}")
print(f" {var.description}")
print(f" Probability: {prob_pct}% | Timeline: {var.timeline_days} days")
print(f" ARR impact: -{var.arrt_impact_pct}% | "
f"Runway impact: -{var.runway_impact_months} months")
print(f" Affected: {', '.join(d.value for d in var.affected_domains)}")
# Combined probability
combined_prob = 1.0
for var in scenario.variables:
combined_prob *= var.probability
print(f"\n Combined probability (all hit): {combined_prob * 100:.1f}%")
# Severity Levels
print(f"\n{'=' * 70}")
print("SEVERITY ANALYSIS")
print("=" * 70)
for severity in Severity:
if severity == Severity.BASE and len(scenario.variables) < 1:
continue
if severity == Severity.STRESS and len(scenario.variables) < 2:
continue
impact = calculate_impact(scenario, severity)
icon = {"base": "🟡", "stress": "🔴", "severe": "💀"}[impact["severity"]]
print(f"\n{icon} {impact['severity'].upper()} SCENARIO")
print(f" Variables: {', '.join(impact['active_variables'])}")
print(f" ARR at risk: {format_currency(impact['arr_at_risk_usd'])} "
f"({impact['arr_at_risk_pct']}%)")
print(f" Projected ARR: {format_currency(impact['projected_arr_usd'])}")
print(f" Runway: {impact['runway_months']} months "
f"({impact['runway_change']:+.1f} months)")
print(f" Burn multiple: {impact['new_burn_multiple']}x")
if impact['cascade_multiplier'] > 1.0:
print(f" Cascade amplifier: {impact['cascade_multiplier']}x "
f"(domains interact)")
print(f" Board escalation: {'⚠️ YES' if impact['board_escalation_required'] else 'No'}")
print(f" Existential risk: {'🚨 YES' if impact['existential_risk'] else 'No'}")
# Cascade Map
if scenario.cascades:
print(f"\n{'=' * 70}")
print("CASCADE MAP")
print("=" * 70)
for i, cascade in enumerate(scenario.cascades, 1):
print(f"\n [{i}] {cascade.trigger_domain.value}")
print(f" ↓ {cascade.mechanism}")
print(f" → {cascade.caused_domain.value} "
f"(amplified {cascade.severity_multiplier}x)")
# Early Warning Triggers
print(f"\n{'=' * 70}")
print("EARLY WARNING TRIGGERS")
print("=" * 70)
triggers = identify_triggers(scenario.variables)
for trigger in triggers:
print(f"\n 📡 {trigger['variable']}")
print(f" Watch: {trigger['timeline']}")
print(f" Owner: {trigger['response_owner']}")
for signal in trigger['signals']:
print(f" • {signal}")
# Hedges
if scenario.hedges:
print(f"\n{'=' * 70}")
print("HEDGING STRATEGIES (act now)")
print("=" * 70)
sorted_hedges = sorted(scenario.hedges,
key=lambda h: h.reduces_probability, reverse=True)
for hedge in sorted_hedges:
print(f"\n ✅ {hedge.action}")
print(f" Cost: {format_currency(hedge.cost_usd)}/year | "
f"Owner: {hedge.owner} | Deadline: {hedge.deadline_days} days")
print(f" Impact: {hedge.impact_description}")
print(f" Risk reduction: {int(hedge.reduces_probability * 100)}%")
print(f"\n{'=' * 70}\n")
def build_sample_scenario() -> Scenario:
"""Sample: Customer churn + fundraise miss compound scenario."""
variables = [
Variable(
name="Top customer churn",
description="Largest customer (28% of ARR) gives 60-day termination notice",
probability=0.15,
arrt_impact_pct=28.0,
runway_impact_months=4.0,
affected_domains=[
Domain.FINANCIAL, Domain.REVENUE, Domain.OPERATIONS
],
timeline_days=60,
),
Variable(
name="Series A delayed 6 months",
description="Fundraise process extends beyond target close; bridge required",
probability=0.25,
arrt_impact_pct=0.0, # No ARR impact directly
runway_impact_months=3.0, # Bridge terms reduce effective runway
affected_domains=[
Domain.FINANCIAL, Domain.PEOPLE, Domain.OPERATIONS
],
timeline_days=120,
),
Variable(
name="Lead engineer resigns",
description="Engineering lead + 1 senior resign during uncertainty",
probability=0.20,
arrt_impact_pct=5.0, # Roadmap slip causes some revenue impact
runway_impact_months=1.0,
affected_domains=[
Domain.ENGINEERING, Domain.PRODUCT, Domain.REVENUE
],
timeline_days=30,
),
]
cascades = [
CascadeEffect(
trigger_domain=Domain.REVENUE,
caused_domain=Domain.FINANCIAL,
mechanism="ARR loss increases burn multiple; runway compresses",
severity_multiplier=1.3,
),
CascadeEffect(
trigger_domain=Domain.FINANCIAL,
caused_domain=Domain.PEOPLE,
mechanism="Hiring freeze + uncertainty triggers attrition risk",
severity_multiplier=1.2,
),
CascadeEffect(
trigger_domain=Domain.PEOPLE,
caused_domain=Domain.PRODUCT,
mechanism="Engineering attrition slips roadmap; customer value drops",
severity_multiplier=1.15,
),
]
hedges = [
Hedge(
action="Establish $750K revolving credit line",
cost_usd=7_500,
impact_description="Buys 4+ months if churn hits before fundraise closes",
owner="CFO",
deadline_days=45,
reduces_probability=0.40,
),
Hedge(
action="12-month retention bonuses for 3 key engineers",
cost_usd=90_000,
impact_description="Locks critical talent through fundraise uncertainty",
owner="CHRO",
deadline_days=30,
reduces_probability=0.60,
),
Hedge(
action="Diversify revenue: reduce top customer to <20% ARR in 2 quarters",
cost_usd=0,
impact_description="Structural risk reduction; takes 6+ months to achieve",
owner="CRO",
deadline_days=14,
reduces_probability=0.30,
),
Hedge(
action="Accelerate fundraise: start parallel process, compress timeline",
cost_usd=15_000,
impact_description="Closes before scenarios compound; reduces bridge risk",
owner="CEO",
deadline_days=7,
reduces_probability=0.35,
),
]
return Scenario(
name="Customer Churn + Fundraise Miss + Eng Attrition",
variables=variables,
cascades=cascades,
hedges=hedges,
current_arr_usd=2_000_000,
current_runway_months=14,
monthly_burn_usd=140_000,
)
def interactive_mode() -> Scenario:
"""Simple CLI for building a custom scenario."""
print("\n🔴 SCENARIO WAR ROOM — Custom Scenario Builder")
print("=" * 50)
print("Define up to 3 scenario variables.\n")
name = input("Scenario name: ").strip() or "Custom Scenario"
current_arr = int(input("Current ARR ($): ").strip() or "2000000")
current_runway = int(input("Current runway (months): ").strip() or "14")
monthly_burn = int(current_arr / current_runway) if current_runway > 0 else 140000
variables = []
for i in range(1, 4):
print(f"\nVariable {i} (press Enter to skip):")
var_name = input(" Name: ").strip()
if not var_name:
break
desc = input(" Description: ").strip() or var_name
prob = float(input(" Probability (0-100%): ").strip() or "20") / 100
arr_impact = float(input(" ARR impact (%): ").strip() or "10")
runway_impact = float(input(" Runway impact (months): ").strip() or "2")
timeline = int(input(" Timeline (days): ").strip() or "90")
variables.append(Variable(
name=var_name,
description=desc,
probability=prob,
arrt_impact_pct=arr_impact,
runway_impact_months=runway_impact,
affected_domains=[Domain.FINANCIAL, Domain.REVENUE],
timeline_days=timeline,
))
if not variables:
print("No variables defined. Using sample scenario.")
return build_sample_scenario()
return Scenario(
name=name,
variables=variables,
cascades=[],
hedges=[],
current_arr_usd=current_arr,
current_runway_months=current_runway,
monthly_burn_usd=monthly_burn,
)
def main():
print("\n🔴 SCENARIO WAR ROOM")
print("Multi-variable cascade modeler for startup adversity planning\n")
if "--interactive" in sys.argv or "-i" in sys.argv:
scenario = interactive_mode()
else:
print("Running sample scenario: Customer Churn + Fundraise Miss + Eng Attrition")
print("(Use --interactive or -i for custom scenario)\n")
scenario = build_sample_scenario()
print_report(scenario)
if "--json" in sys.argv:
results = {}
for severity in Severity:
impact = calculate_impact(scenario, severity)
results[severity.value] = impact
print(json.dumps(results, indent=2))
if __name__ == "__main__":
main()
FILE:strategic-alignment/SKILL.md
---
name: "strategic-alignment"
description: "Cascades strategy from boardroom to individual contributor. Detects and fixes misalignment between company goals and team execution. Covers strategy articulation, cascade mapping, orphan goal detection, silo identification, communication gap analysis, and realignment protocols. Use when teams are pulling in different directions, OKRs don't connect, departments optimize locally at company expense, or when user mentions alignment, strategy cascade, silo, conflicting OKRs, or strategy communication."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: strategic-alignment
updated: 2026-03-05
python-tools: alignment_checker.py
frameworks: alignment-playbook
---
# Strategic Alignment Engine
Strategy fails at the cascade, not the boardroom. This skill detects misalignment before it becomes dysfunction and builds systems that keep strategy connected from CEO to individual contributor.
## Keywords
strategic alignment, strategy cascade, OKR alignment, orphan OKRs, conflicting goals, silos, communication gap, department alignment, alignment checker, strategy articulation, cross-functional, goal cascade, misalignment, alignment score
## Quick Start
```bash
python scripts/alignment_checker.py # Check OKR alignment: orphans, conflicts, coverage gaps
```
## Core Framework
The alignment problem: **The further a goal gets from the strategy that created it, the less likely it reflects the original intent.** This is the organizational telephone game. It happens at every stage. The question is how bad it is and how to fix it.
### Step 1: Strategy Articulation Test
Before checking cascade, check the source. Ask five people from five different teams:
**"What is the company's most important strategic priority right now?"**
**Scoring:**
- All five give the same answer: ✅ Articulation is clear
- 3–4 give similar answers: 🟡 Loose alignment — clarify and communicate
- < 3 agree: 🔴 Strategy isn't clear enough to cascade. Fix this before fixing cascade.
**Format test:** The strategy should be statable in one sentence. If leadership needs a paragraph, teams won't internalize it.
- ❌ "We focus on product-led growth while maintaining enterprise relationships and expanding our international presence and investing in platform capabilities"
- ✅ "Win the mid-market healthcare segment in DACH before Series B"
### Step 2: Cascade Mapping
Map the flow from company strategy → each level of the organization.
```
Company level: OKR-1, OKR-2, OKR-3
↓
Dept level: Sales OKRs, Eng OKRs, Product OKRs, CS OKRs
↓
Team level: Team A OKRs, Team B OKRs...
↓
Individual: Personal goals / rocks
```
**For each goal at every level, ask:**
- Which company-level goal does this support?
- If this goal is 100% achieved, how much does it move the company goal?
- Is the connection direct or theoretical?
### Step 3: Alignment Detection
Three failure patterns:
**Orphan goals:** Team or individual goals that don't connect to any company goal.
- Symptom: "We've been working on this for a quarter and nobody above us seems to care"
- Root cause: Goals set bottom-up or from last quarter's priorities without reconciling to current company OKRs
- Fix: Connect or cut. Every goal needs a parent.
**Conflicting goals:** Two teams' goals, when both succeed, create a worse outcome.
- Classic example: Sales commits to volume contracts (revenue), CS is measured on satisfaction scores. Sales closes bad-fit customers; CS scores tank.
- Fix: Cross-functional OKR review before quarter begins. Shared metrics where teams interact.
**Coverage gaps:** Company has 3 OKRs. 5 teams support OKR-1, 2 support OKR-2, 0 support OKR-3.
- Symptom: Company OKR-3 consistently misses; nobody owns it
- Fix: Explicit ownership assignment. If no team owns a company OKR, it won't happen.
See `scripts/alignment_checker.py` for automated detection against your JSON-formatted OKRs.
### Step 4: Silo Identification
Silos exist when teams optimize for local metrics at the expense of company metrics.
**Silo signals:**
- A department consistently hits their goals while the company misses
- Teams don't know what other teams are working on
- "That's not our problem" is a common phrase
- Escalations only flow up; coordination never flows sideways
- Data isn't shared between teams that depend on each other
**Silo root causes:**
1. **Incentive misalignment:** Teams rewarded for local metrics don't optimize for company metrics
2. **No shared goals:** When teams share a goal, they coordinate. When they don't, they drift.
3. **No shared language:** Engineering doesn't understand sales metrics; sales doesn't understand technical debt
4. **Geography or time zones:** Silos accelerate when teams don't interact organically
**Silo measurement:**
- How often do teams request something from each other vs. proceed independently?
- How much time does it take to resolve a cross-functional issue?
- Can a team member describe the current priorities of an adjacent team?
### Step 5: Communication Gap Analysis
What the CEO says ≠ what teams hear. The gap grows with company size.
**The message decay model:**
- CEO communicates strategy at all-hands → managers filter through their lens → teams receive modified version → individuals interpret further
**Gap sources:**
- **Ambiguity:** Strategy stated at too high a level ("grow the business") lets each team fill in their own interpretation
- **Frequency:** One all-hands per quarter isn't enough repetition to change behavior
- **Medium mismatch:** Long written strategy doc for teams that respond to visual communication
- **Trust deficit:** Teams don't believe the strategy is real ("we've heard this before")
**Gap detection:**
- Run the Step 1 articulation test across all levels
- Compare what leadership thinks they communicated vs. what teams say they heard
- Survey: "What changed about how you work since the last strategy update?"
### Step 6: Realignment Protocol
How to fix misalignment without calling it a "realignment" (which creates fear).
**Step 6a: Don't start with what's wrong**
Starting with "here's our misalignment" creates defensiveness. Start with "here's where we're heading and I want to make sure we're connected."
**Step 6b: Re-cascade in a workshop, not a memo**
Alignment workshops are more effective than documents. Get company-level OKR owners and department leads in a room. Map connections. Find gaps together.
**Step 6c: Fix incentives before fixing goals**
If department heads are rewarded for local metrics that conflict with company goals, no amount of goal-setting fixes the problem. The incentive structure must change first.
**Step 6d: Install a quarterly alignment check**
After fixing, prevent recurrence. See `references/alignment-playbook.md` for quarterly cadence.
---
## Alignment Score
A quick health check. Score each area 0–10:
| Area | Question | Score |
|------|----------|-------|
| Strategy clarity | Can 5 people from different teams state the strategy consistently? | /10 |
| Cascade completeness | Do all team goals connect to company goals? | /10 |
| Conflict detection | Have cross-team OKR conflicts been reviewed and resolved? | /10 |
| Coverage | Does each company OKR have explicit team ownership? | /10 |
| Communication | Do teams' behaviors reflect the strategy (not just their stated understanding)? | /10 |
**Total: __ / 50**
| Score | Status |
|-------|--------|
| 45–50 | Excellent. Maintain the system. |
| 35–44 | Good. Address specific weak areas. |
| 20–34 | Misalignment is costing you. Immediate attention required. |
| < 20 | Strategic drift. Treat as crisis. |
---
## Key Questions for Alignment
- "Ask your newest team member: what is the most important thing the company is trying to achieve right now?"
- "Which company OKR does your team's top priority support? Can you trace the connection?"
- "When Team A and Team B both hit their goals, does the company always win? Are there scenarios where they don't?"
- "What changed in how your team works since the last strategy update?"
- "Name a decision made last week that was influenced by the company strategy."
## Red Flags
- Teams consistently hit goals while company misses targets
- Cross-functional projects take 3x longer than expected (coordination failure)
- Strategy updated quarterly but team priorities don't change
- "That's a leadership problem, not our problem" attitude at the team level
- New initiatives announced without connecting them to existing OKRs
- Department heads optimize for headcount or budget rather than company outcomes
## Integration with Other C-Suite Roles
| When... | Work with... | To... |
|---------|-------------|-------|
| New strategy is set | CEO + COO | Cascade into quarterly rocks before announcing |
| OKR cycle starts | COO | Run cross-team conflict check before finalizing |
| Team consistently misses goals | CHRO | Diagnose: capability gap or alignment gap? |
| Silo identified | COO | Design shared metrics or cross-functional OKRs |
| Post-M&A | CEO + Culture Architect | Detect strategy conflicts between merged entities |
## Detailed References
- `scripts/alignment_checker.py` — Automated OKR alignment analysis (orphans, conflicts, coverage)
- `references/alignment-playbook.md` — Cascade techniques, quarterly alignment check, common patterns
FILE:strategic-alignment/references/alignment-playbook.md
# Strategic Alignment Playbook
Techniques for cascading strategy, detecting drift, and maintaining alignment at scale.
---
## 1. Strategy Cascade Techniques
### The One-Page Strategy Filter
Before cascading, compress strategy to one page. If it doesn't fit on one page, it's not clear enough to cascade.
**Template:**
```
Company Strategy — [Quarter/Year]
─────────────────────────────────
WHERE WE'RE GOING (6-word vision):
─────────────────────────────────
TOP 3 PRIORITIES THIS QUARTER:
1. [Priority] — owned by: [name]
2. [Priority] — owned by: [name]
3. [Priority] — owned by: [name]
─────────────────────────────────
WHAT WE'RE NOT DOING:
- [Deprioritized initiative]
- [Deferred until next quarter]
─────────────────────────────────
HOW WE MEASURE SUCCESS:
- [Key metric 1]
- [Key metric 2]
- [Key metric 3]
```
The "What we're NOT doing" section is as important as the priorities. Without it, every team adds their own priorities.
### The Cascade Workshop
**Step 1: Company OKR owners present to all department leads (60 min)**
Walk through each company OKR. Explain the "why" behind each — the reasoning, not just the what.
**Step 2: Department leads draft their OKRs in response (90 min)**
Each department answers: "Given these company OKRs, what is our department uniquely positioned to contribute?"
**Step 3: Cross-check for conflicts and gaps (60 min)**
All departments present their draft OKRs. Flag: Which company OKR has no department support? Which two departments might conflict?
**Step 4: Resolve before publishing (30 min)**
Assign missing coverage. Negotiate shared metrics for conflict-prone areas.
**Step 5: Cascade to teams and individuals**
Each department lead runs the same workshop with their teams within 1 week.
### Cascade rules
1. **Bottom-up complements top-down.** Some goals should emerge from teams, not be handed down. Reserve 20–30% of each team's OKRs for team-defined goals that connect to company direction.
2. **Every team goal needs a parent.** If you can't draw a line from a team goal to a company OKR, the goal is either wrong or the company OKR is incomplete.
3. **Cascade the WHY, not just the WHAT.** "Achieve €800K ARR in DACH" without context produces different behaviors than "Achieve €800K ARR in DACH to demonstrate product-market fit before our Series B in Q4."
---
## 2. The Telephone Game Problem and How to Beat It
### The problem
A study by a leadership development firm found that:
- 95% of employees can't name their company's top strategic priorities
- Of those who can, 60% interpret them differently than leadership intended
This is the telephone game at scale. It's not a communication failure — it's an organizational physics problem.
### Why strategy degrades
**Layer 1 → Layer 2:** Managers interpret strategy through their own context. "Focus on efficiency" becomes "cut costs" in Operations and "ship fewer features" in Engineering.
**Layer 2 → Layer 3:** Teams interpret their manager's interpretation. The original strategy is now third-hand.
**Written vs. oral:** Written documents persist. Oral communication changes with each telling. Most cascade happens orally.
**Recency bias:** The last thing said overwrites earlier context. A strategy set in January doesn't survive a September all-hands that emphasizes something different.
### How to beat it
**Repetition is the solution, not the problem.** Most leaders communicate a strategy once and assume it was received. Research on organizational communication suggests 7+ exposures before a message changes behavior.
**Vary the format.** Same message in writing, verbal, visual, story, and example. Different people receive different formats.
**Create shared vocabulary.** If everyone calls the strategy by the same name, it creates a reference point. "We're in DACH focus mode" is more transmissible than a paragraph.
**Test comprehension, not communication.** Ask random team members: "What are our top 3 priorities right now?" The answer tells you whether cascade worked, not whether you communicated.
**Use stories, not slides.** "Here's a decision we made last week that's a perfect example of the strategy" is more memorable than restating the OKR.
---
## 3. Cross-Functional OKR Design
Silos form when teams have no shared goals. The fix: design OKRs that require multiple teams to cooperate.
### Shared ownership OKR
**Format:**
```
Objective: [What we'll achieve together]
Primary owner: [Team A]
Contributing owner: [Team B]
Key Results:
- KR owned by Team A: [Metric]
- KR owned by Team B: [Metric]
- Shared KR (both teams): [Metric that requires both]
```
**Example:**
```
Objective: Launch the partner API and acquire first 3 integrations
Primary owner: Engineering
Contributing owner: Business Development
KR 1 (Engineering): API v1 live with 100% documentation by Week 8
KR 2 (BD): 3 signed partner integration agreements by EoQ
KR 3 (Shared): First partner integration live and in production by EoQ
```
### Cross-functional conflict metric
When two teams' goals are potentially in conflict, add a shared guardrail metric:
**Example:**
- Sales goal: 15 new logos
- CS goal: Churn < 2%
- **Shared guardrail:** New customer 90-day churn < 5% (Sales can't close unqualified customers; CS can't blame Sales for their churn)
---
## 4. Alignment Check Cadence
### Quarterly alignment check (before OKR planning)
Run this before setting next quarter's OKRs:
**Week −2 (2 weeks before quarter start):**
- All teams review current OKRs: Which are we hitting? Which are we missing?
- Run the alignment checker: Orphans? Gaps? Conflicts?
**Week −1:**
- Cascade workshop: Company sets next quarter's OKRs
- Cross-functional conflict review
- Coverage gap assignment
**Week 1 of new quarter:**
- All teams have finalized OKRs with documented parent company OKRs
- Shared OKRs documented with co-owners
- Guardrail metrics in place for known conflict areas
### Monthly alignment pulse
One question added to monthly department reviews:
**"How is our work moving the company-level OKRs? What's the connection?"**
Force each team lead to articulate the link. If they struggle, the cascade has broken.
### Weekly alignment signal
One question added to leadership L10 meetings:
**"Is there anything happening in our team that's at odds with the company strategy?"**
This creates a standing invitation to surface misalignment before it compounds.
---
## 5. Common Misalignment Patterns by Company Stage
### Seed stage (< 20 people)
**Pattern:** Everyone knows everything, alignment is informal. You don't need OKRs — you have daily contact.
**Risk:** Informal alignment breaks when you hire past 15 people and not everyone is in every conversation.
**Fix:** Start documenting strategy at 10–12 people, before it's painful. Establishing the habit early is easier than retrofitting at 50.
### Early growth (20–60 people)
**Pattern:** Functions are forming. Sales, Product, Engineering operate somewhat independently. Communication slows.
**Common misalignment:** Engineering builds features that Sales didn't ask for. Sales promises features Engineering hasn't planned.
**Fix:** Introduce a shared quarterly planning session. Sales and Product review the roadmap together. Engineering and Sales share a customer pipeline update monthly.
### Scaling (60–200 people)
**Pattern:** Multiple layers of management. Strategy takes longer to reach ICs. Managers filter differently.
**Common misalignment:** Department heads optimize their own metrics. Cross-functional projects stall because nobody owns the intersection.
**Fix:** Cross-functional OKRs. Shared metrics. An explicit alignment check in the quarterly planning process (use the alignment_checker.py script).
### Large (200+ people)
**Pattern:** Sub-strategies form. Business units, geographies, and product lines develop their own goals that drift from company strategy over time.
**Common misalignment:** Business unit A and Business unit B compete for the same customer segment. Platform team builds for internal use-cases that differ from external product direction.
**Fix:** Annual strategy alignment summit across business units. Centralized OKR system with visible cross-functional connections. Dedicated alignment role (often the COO or Chief of Staff).
FILE:strategic-alignment/scripts/alignment_checker.py
#!/usr/bin/env python3
"""
Strategic Alignment Checker
Detects misalignment in OKR structures:
- Orphan OKRs: team goals with no connection to company goals
- Conflicting OKRs: team goals that may work against each other
- Coverage gaps: company goals with insufficient team support
Input: JSON file with company and team OKRs
Output: Alignment score, gap report, conflict map
Usage:
python alignment_checker.py # Run with sample data
python alignment_checker.py --file my_okrs.json # Run with your data
python alignment_checker.py --sample # Print sample JSON format
"""
import json
import sys
import argparse
from collections import defaultdict
# ─────────────────────────────────────────────
# Sample data
# ─────────────────────────────────────────────
SAMPLE_DATA = {
"quarter": "Q2 2026",
"company": {
"name": "Acme Corp",
"okrs": [
{
"id": "C1",
"objective": "Win mid-market DACH healthcare segment",
"key_results": [
"Reach 50 paying customers in DACH by EoQ",
"Achieve €800K ARR in DACH",
"Net Revenue Retention > 110%"
]
},
{
"id": "C2",
"objective": "Ship the platform API to unlock partner integrations",
"key_results": [
"API v1 launched with 3 partner integrations",
"API documentation coverage: 100% of endpoints",
"< 200ms P95 response time under load"
]
},
{
"id": "C3",
"objective": "Build a capital-efficient growth engine",
"key_results": [
"CAC payback period < 12 months",
"Burn multiple < 1.5x",
"Revenue per employee up 20% vs Q1"
]
}
]
},
"teams": [
{
"name": "Sales",
"okrs": [
{
"id": "S1",
"objective": "Hit DACH new business targets",
"parent_company_okr_id": "C1",
"key_results": [
"Close 15 new DACH logos",
"Pipeline coverage: 3x of target",
"Average deal size > €18K ARR"
],
"potential_conflicts": ["C3", "CS2"]
},
{
"id": "S2",
"objective": "Expand into Austria market",
"parent_company_okr_id": None, # ORPHAN — no company OKR parent
"key_results": [
"5 qualified meetings with Austrian prospects",
"1 pilot signed in Austria"
],
"potential_conflicts": []
}
]
},
{
"name": "Engineering",
"okrs": [
{
"id": "E1",
"objective": "Deliver API v1 on schedule",
"parent_company_okr_id": "C2",
"key_results": [
"API v1 feature complete by Week 8",
"Zero critical bugs at launch",
"P95 latency < 200ms under 500 RPS"
],
"potential_conflicts": []
},
{
"id": "E2",
"objective": "Reduce infrastructure cost by 30%",
"parent_company_okr_id": "C3",
"key_results": [
"Migrate 3 services to spot instances",
"Decommission legacy DB cluster",
"Monthly infra cost < €12K"
],
"potential_conflicts": []
},
{
"id": "E3",
"objective": "Achieve zero-downtime deployments",
"parent_company_okr_id": None, # ORPHAN
"key_results": [
"Implement blue-green deployment pipeline",
"Deployment success rate > 99.5%"
],
"potential_conflicts": []
}
]
},
{
"name": "Customer Success",
"okrs": [
{
"id": "CS1",
"objective": "Drive retention and expansion in DACH",
"parent_company_okr_id": "C1",
"key_results": [
"NRR > 110% for DACH cohort",
"Churn < 2% gross monthly",
"CSAT score > 4.5/5"
],
"potential_conflicts": []
},
{
"id": "CS2",
"objective": "Reduce support ticket volume by 40%",
"parent_company_okr_id": "C3",
"key_results": [
"Launch self-serve knowledge base",
"Ticket deflection rate > 35%",
"Time-to-first-response < 2 hours"
],
"potential_conflicts": ["S1"] # Volume close pressure → more bad-fit customers → more tickets
}
]
},
{
"name": "Marketing",
"okrs": [
{
"id": "M1",
"objective": "Generate DACH pipeline to support sales targets",
"parent_company_okr_id": "C1",
"key_results": [
"€2.4M qualified pipeline from DACH",
"30 qualified demo requests from target ICP",
"CAC from inbound < €4K"
],
"potential_conflicts": []
}
]
}
],
"known_conflicts": [
{
"team_a": "Sales",
"okr_a": "S1",
"team_b": "Customer Success",
"okr_b": "CS2",
"description": "Sales closing volume deals to hit number may include poor-fit customers, increasing CS ticket load and reducing CSAT — directly conflicting with CS ticket reduction target."
}
]
}
# ─────────────────────────────────────────────
# Analysis functions
# ─────────────────────────────────────────────
def get_all_company_okr_ids(data):
return {okr["id"] for okr in data["company"]["okrs"]}
def detect_orphans(data, company_ids):
"""Find team OKRs with no parent company OKR."""
orphans = []
for team in data["teams"]:
for okr in team["okrs"]:
if okr.get("parent_company_okr_id") is None:
orphans.append({
"team": team["name"],
"okr_id": okr["id"],
"objective": okr["objective"]
})
elif okr["parent_company_okr_id"] not in company_ids:
orphans.append({
"team": team["name"],
"okr_id": okr["id"],
"objective": okr["objective"],
"note": f"References non-existent company OKR: {okr['parent_company_okr_id']}"
})
return orphans
def detect_coverage_gaps(data, company_ids):
"""Find company OKRs with no team support."""
coverage = defaultdict(list)
for team in data["teams"]:
for okr in team["okrs"]:
parent = okr.get("parent_company_okr_id")
if parent and parent in company_ids:
coverage[parent].append({
"team": team["name"],
"okr_id": okr["id"],
"objective": okr["objective"]
})
gaps = []
over_indexed = []
for company_okr in data["company"]["okrs"]:
cid = company_okr["id"]
supporting = coverage.get(cid, [])
entry = {
"company_okr_id": cid,
"objective": company_okr["objective"],
"supporting_team_count": len(supporting),
"supporting_teams": [s["team"] for s in supporting]
}
if len(supporting) == 0:
gaps.append(entry)
elif len(supporting) >= 4:
over_indexed.append(entry)
return gaps, over_indexed, coverage
def detect_conflicts(data):
"""Surface declared and potential OKR conflicts."""
conflicts = []
# Use declared known_conflicts
for conflict in data.get("known_conflicts", []):
conflicts.append({
"type": "declared",
"team_a": conflict["team_a"],
"okr_a": conflict["okr_a"],
"team_b": conflict["team_b"],
"okr_b": conflict["okr_b"],
"description": conflict["description"]
})
# Use potential_conflicts fields on OKRs for cross-reference
okr_index = {}
for team in data["teams"]:
for okr in team["okrs"]:
okr_index[okr["id"]] = {"team": team["name"], "objective": okr["objective"]}
for team in data["teams"]:
for okr in team["okrs"]:
for conflict_id in okr.get("potential_conflicts", []):
if conflict_id in okr_index:
target = okr_index[conflict_id]
# Avoid duplicate (A→B and B→A)
already_declared = any(
(c["okr_a"] == okr["id"] and c["okr_b"] == conflict_id) or
(c["okr_a"] == conflict_id and c["okr_b"] == okr["id"])
for c in conflicts
)
if not already_declared:
conflicts.append({
"type": "potential",
"team_a": team["name"],
"okr_a": okr["id"],
"team_b": target["team"],
"okr_b": conflict_id,
"description": f"Potential conflict between '{okr['objective']}' and '{target['objective']}' — review recommended"
})
return conflicts
def compute_alignment_score(data, orphans, gaps, conflicts, coverage):
"""Score overall alignment from 0–100."""
total_team_okrs = sum(len(t["okrs"]) for t in data["teams"])
total_company_okrs = len(data["company"]["okrs"])
orphan_penalty = (len(orphans) / max(total_team_okrs, 1)) * 30
gap_penalty = (len(gaps) / max(total_company_okrs, 1)) * 30
conflict_penalty = min(len(conflicts) * 10, 30)
score = max(0, 100 - orphan_penalty - gap_penalty - conflict_penalty)
return round(score)
def score_label(score):
if score >= 85:
return "✅ Excellent"
elif score >= 70:
return "🟡 Moderate misalignment"
elif score >= 50:
return "🟠 Significant misalignment"
else:
return "🔴 Critical misalignment"
# ─────────────────────────────────────────────
# Report generation
# ─────────────────────────────────────────────
def print_report(data, orphans, gaps, over_indexed, conflicts, coverage, score):
sep = "─" * 60
print(f"\n{'═' * 60}")
print(f" STRATEGIC ALIGNMENT REPORT — {data.get('quarter', 'Unknown Quarter')}")
print(f" Company: {data['company']['name']}")
print(f"{'═' * 60}\n")
print(f" ALIGNMENT SCORE: {score}/100 {score_label(score)}\n")
print(sep)
# Company OKRs summary
print("\n📋 COMPANY OKRs\n")
for okr in data["company"]["okrs"]:
supporting = coverage.get(okr["id"], [])
teams_str = ", ".join(s["team"] for s in supporting) if supporting else "⚠️ NONE"
print(f" [{okr['id']}] {okr['objective']}")
print(f" Supported by: {teams_str}")
print()
print(sep)
# Orphan OKRs
print(f"\n🔍 ORPHAN OKRs ({len(orphans)} found)\n")
if orphans:
for o in orphans:
note = f" — {o.get('note', 'No parent company OKR assigned')}"
print(f" ⚠️ [{o['okr_id']}] {o['team']}: {o['objective']}")
print(f" Issue: {note}")
print()
print(" → Action: Connect each orphan to a company OKR, or deprioritize it.")
else:
print(" ✅ None found. All team OKRs connect to company OKRs.")
print()
print(sep)
# Coverage gaps
print(f"\n🕳️ COVERAGE GAPS ({len(gaps)} company OKRs with zero team support)\n")
if gaps:
for g in gaps:
print(f" 🔴 [{g['company_okr_id']}] {g['objective']}")
print(f" No team is working on this. It will not be achieved.")
print()
print(" → Action: Assign at least one team owner to each unowned company OKR.")
else:
print(" ✅ All company OKRs have at least one team supporting them.")
print()
if over_indexed:
print(f" 📊 OVER-INDEXED OKRs ({len(over_indexed)} company OKRs with 4+ teams)\n")
for o in over_indexed:
print(f" [{o['company_okr_id']}] {o['objective']}")
print(f" {o['supporting_team_count']} teams: {', '.join(o['supporting_teams'])}")
print()
print(" → Note: High coverage isn't necessarily bad, but check if under-covered OKRs are being neglected.")
print(sep)
# Conflicts
print(f"\n⚡ CONFLICTING OKRs ({len(conflicts)} found)\n")
if conflicts:
for i, c in enumerate(conflicts, 1):
label = "🔴 Declared" if c["type"] == "declared" else "🟡 Potential"
print(f" {label} Conflict #{i}")
print(f" {c['team_a']} [{c['okr_a']}] ↔ {c['team_b']} [{c['okr_b']}]")
print(f" {c['description']}")
print()
print(" → Action: For each conflict, design a shared metric or shared constraint that prevents local optimization at company expense.")
else:
print(" ✅ No declared or potential conflicts detected.")
print()
print(sep)
# Summary
print("\n📊 SUMMARY\n")
total_team_okrs = sum(len(t["okrs"]) for t in data["teams"])
total_company_okrs = len(data["company"]["okrs"])
print(f" Company OKRs: {total_company_okrs}")
print(f" Team OKRs: {total_team_okrs}")
print(f" Orphan OKRs: {len(orphans)}")
print(f" Coverage gaps: {len(gaps)} of {total_company_okrs} company OKRs have no team support")
print(f" Conflicts: {len(conflicts)}")
print(f" Alignment score: {score}/100 {score_label(score)}")
print()
if score < 70:
print(" ⚠️ RECOMMENDED ACTIONS:")
if orphans:
print(f" 1. Resolve {len(orphans)} orphan OKR(s) — connect to company goals or cut")
if gaps:
print(f" 2. Assign team owners to {len(gaps)} uncovered company OKR(s)")
if conflicts:
print(f" 3. Address {len(conflicts)} conflict(s) with shared metrics or constraints")
print(" 4. Run a cross-functional OKR review before next quarter begins")
print()
print(f"{'═' * 60}\n")
# ─────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Strategic OKR Alignment Checker")
parser.add_argument("--file", help="Path to JSON file with OKR data")
parser.add_argument("--sample", action="store_true", help="Print sample JSON format and exit")
args = parser.parse_args()
if args.sample:
print(json.dumps(SAMPLE_DATA, indent=2))
return
if args.file:
try:
with open(args.file, "r") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File '{args.file}' not found.")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.file}': {e}")
sys.exit(1)
else:
print("No file provided. Running with sample data.\n")
print("To use your own data: python alignment_checker.py --file your_okrs.json")
print("To see the expected JSON format: python alignment_checker.py --sample\n")
data = SAMPLE_DATA
# Run analysis
company_ids = get_all_company_okr_ids(data)
orphans = detect_orphans(data, company_ids)
gaps, over_indexed, coverage = detect_coverage_gaps(data, company_ids)
conflicts = detect_conflicts(data)
score = compute_alignment_score(data, orphans, gaps, conflicts, coverage)
# Print report
print_report(data, orphans, gaps, over_indexed, conflicts, coverage, score)
if __name__ == "__main__":
main()
Curate Claude Code's auto-memory into durable project knowledge. Analyze MEMORY.md for patterns, promote proven learnings to CLAUDE.md and .claude/rules/, ex...
---
name: auto-memory-pro
description: "Curate Claude Code's auto-memory into durable project knowledge. Analyze MEMORY.md for patterns, promote proven learnings to CLAUDE.md and .claude/rules/, extract recurring solutions into reusable skills. Use when: (1) reviewing what Claude has learned about your project, (2) graduating a pattern from notes to enforced rules, (3) turning a debugging solution into a skill, (4) checking memory health and capacity."
---
# Self-Improving Agent
> Auto-memory captures. This plugin curates.
Claude Code's auto-memory (v2.1.32+) automatically records project patterns, debugging insights, and your preferences in `MEMORY.md`. This plugin adds the intelligence layer: it analyzes what Claude has learned, promotes proven patterns into project rules, and extracts recurring solutions into reusable skills.
## Quick Reference
| Command | What it does |
|---------|-------------|
| `/si:review` | Analyze MEMORY.md — find promotion candidates, stale entries, consolidation opportunities |
| `/si:promote` | Graduate a pattern from MEMORY.md → CLAUDE.md or `.claude/rules/` |
| `/si:extract` | Turn a proven pattern into a standalone skill |
| `/si:status` | Memory health dashboard — line counts, topic files, recommendations |
| `/si:remember` | Explicitly save important knowledge to auto-memory |
## How It Fits Together
```
┌─────────────────────────────────────────────────────────┐
│ Claude Code Memory Stack │
├─────────────┬──────────────────┬────────────────────────┤
│ CLAUDE.md │ Auto Memory │ Session Memory │
│ (you write)│ (Claude writes)│ (Claude writes) │
│ Rules & │ MEMORY.md │ Conversation logs │
│ standards │ + topic files │ + continuity │
│ Full load │ First 200 lines│ Contextual load │
├─────────────┴──────────────────┴────────────────────────┤
│ ↑ /si:promote ↑ /si:review │
│ Self-Improving Agent (this plugin) │
│ ↓ /si:extract ↓ /si:remember │
├─────────────────────────────────────────────────────────┤
│ .claude/rules/ │ New Skills │ Error Logs │
│ (scoped rules) │ (extracted) │ (auto-captured)│
└─────────────────────────────────────────────────────────┘
```
## Installation
### Claude Code (Plugin)
```
/plugin marketplace add alirezarezvani/claude-skills
/plugin install self-improving-agent@claude-code-skills
```
### OpenClaw
```bash
clawhub install self-improving-agent
```
### Codex CLI
```bash
./scripts/codex-install.sh --skill self-improving-agent
```
## Memory Architecture
### Where things live
| File | Who writes | Scope | Loaded |
|------|-----------|-------|--------|
| `./CLAUDE.md` | You (+ `/si:promote`) | Project rules | Full file, every session |
| `~/.claude/CLAUDE.md` | You | Global preferences | Full file, every session |
| `~/.claude/projects/<path>/memory/MEMORY.md` | Claude (auto) | Project learnings | First 200 lines |
| `~/.claude/projects/<path>/memory/*.md` | Claude (overflow) | Topic-specific notes | On demand |
| `.claude/rules/*.md` | You (+ `/si:promote`) | Scoped rules | When matching files open |
### The promotion lifecycle
```
1. Claude discovers pattern → auto-memory (MEMORY.md)
2. Pattern recurs 2-3x → /si:review flags it as promotion candidate
3. You approve → /si:promote graduates it to CLAUDE.md or rules/
4. Pattern becomes an enforced rule, not just a note
5. MEMORY.md entry removed → frees space for new learnings
```
## Core Concepts
### Auto-memory is capture, not curation
Auto-memory is excellent at recording what Claude learns. But it has no judgment about:
- Which learnings are temporary vs. permanent
- Which patterns should become enforced rules
- When the 200-line limit is wasting space on stale entries
- Which solutions are good enough to become reusable skills
That's what this plugin does.
### Promotion = graduation
When you promote a learning, it moves from Claude's scratchpad (MEMORY.md) to your project's rule system (CLAUDE.md or `.claude/rules/`). The difference matters:
- **MEMORY.md**: "I noticed this project uses pnpm" (background context)
- **CLAUDE.md**: "Use pnpm, not npm" (enforced instruction)
Promoted rules have higher priority and load in full (not truncated at 200 lines).
### Rules directory for scoped knowledge
Not everything belongs in CLAUDE.md. Use `.claude/rules/` for patterns that only apply to specific file types:
```yaml
# .claude/rules/api-testing.md
---
paths:
- "src/api/**/*.test.ts"
- "tests/api/**/*"
---
- Use supertest for API endpoint testing
- Mock external services with msw
- Always test error responses, not just happy paths
```
This loads only when Claude works with API test files — zero overhead otherwise.
## Agents
### memory-analyst
Analyzes MEMORY.md and topic files to identify:
- Entries that recur across sessions (promotion candidates)
- Stale entries referencing deleted files or old patterns
- Related entries that should be consolidated
- Gaps between what MEMORY.md knows and what CLAUDE.md enforces
### skill-extractor
Takes a proven pattern and generates a complete skill:
- SKILL.md with proper frontmatter
- Reference documentation
- Examples and edge cases
- Ready for `/plugin install` or `clawhub publish`
## Hooks
### error-capture (PostToolUse → Bash)
Monitors command output for errors. When detected, appends a structured entry to auto-memory with:
- The command that failed
- Error output (truncated)
- Timestamp and context
- Suggested category
**Token overhead:** Zero on success. ~30 tokens only when an error is detected.
## Platform Support
| Platform | Memory System | Plugin Works? |
|----------|--------------|---------------|
| Claude Code | Auto-memory (MEMORY.md) | ✅ Full support |
| OpenClaw | workspace/MEMORY.md | ✅ Adapted (reads workspace memory) |
| Codex CLI | AGENTS.md | ✅ Adapted (reads AGENTS.md patterns) |
| GitHub Copilot | `.github/copilot-instructions.md` | ⚠️ Manual promotion only |
## Related
- [Claude Code Memory Docs](https://code.claude.com/docs/en/memory)
- [pskoett/self-improving-agent](https://clawhub.ai/pskoett/self-improving-agent) — inspiration
- [playwright-pro](../playwright-pro/) — sister plugin in this repo
FILE:CLAUDE.md
# Self-Improving Agent — Claude Code Instructions
This plugin helps you curate Claude Code's auto-memory into durable project knowledge.
## Commands
Use the `/si:` namespace for all commands:
- `/si:review` — Analyze auto-memory health and find promotion candidates
- `/si:promote <pattern>` — Graduate a learning to CLAUDE.md or `.claude/rules/`
- `/si:extract <pattern>` — Create a reusable skill from a proven pattern
- `/si:status` — Quick memory health dashboard
- `/si:remember <knowledge>` — Explicitly save something to auto-memory
## How auto-memory works
Claude Code maintains `~/.claude/projects/<project-path>/memory/MEMORY.md` automatically. The first 200 lines load into every session. When it grows too large, Claude moves details into topic files like `debugging.md` or `patterns.md`.
This plugin reads that directory — it never creates its own storage.
## When to use each command
### After completing a feature or debugging session
```
/si:review
```
Check if anything Claude learned should become a permanent rule.
### When a pattern keeps coming up
```
/si:promote "Always run migrations before tests in this project"
```
Moves it from MEMORY.md (background note) to CLAUDE.md (enforced rule).
### When you solved something non-obvious that could help other projects
```
/si:extract "Docker build fix for ARM64 platform mismatch"
```
Creates a standalone skill with SKILL.md, ready to install elsewhere.
### To check memory capacity
```
/si:status
```
Shows line counts, topic files, stale entries, and recommendations.
## Key principle
**Don't fight auto-memory — orchestrate it.**
- Auto-memory is great at capturing patterns. Let it do its job.
- This plugin adds judgment: what's worth keeping, what should be promoted, what's stale.
- Promoted rules in CLAUDE.md have higher priority than MEMORY.md entries.
- Removing promoted entries from MEMORY.md frees space for new learnings.
## Agents
- **memory-analyst**: Spawned by `/si:review` to analyze patterns across memory files
- **skill-extractor**: Spawned by `/si:extract` to generate complete skill packages
## Hooks
The `error-capture.sh` hook fires on `PostToolUse` (Bash only). It detects command failures and appends structured entries to auto-memory. Zero overhead on successful commands.
To enable:
```json
// .claude/settings.json
{
"hooks": {
"PostToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "./skills/self-improving-agent/hooks/error-capture.sh"
}]
}]
}
}
```
FILE:README.md
# Self-Improving Agent
> Auto-memory captures. This plugin curates.
A Claude Code plugin that turns auto-memory into a structured self-improvement loop. Analyze what Claude has learned, promote proven patterns to enforced rules, and extract recurring solutions into reusable skills.
## Why
Claude Code's auto-memory (v2.1.32+) automatically records project patterns in `MEMORY.md`. But it has no judgment about what to keep, what to promote, or when entries go stale. This plugin adds the intelligence layer.
**The difference:**
- **MEMORY.md**: "I noticed this project uses pnpm" (background note, truncated at 200 lines)
- **CLAUDE.md**: "Use pnpm, not npm" (enforced instruction, loaded in full)
Promoting a pattern from memory to rules fundamentally changes how Claude treats it.
## Commands
| Command | What it does |
|---------|-------------|
| `/si:review` | Analyze auto-memory — find promotion candidates, stale entries, health metrics |
| `/si:promote` | Graduate a pattern from MEMORY.md → CLAUDE.md or `.claude/rules/` |
| `/si:extract` | Turn a recurring pattern into a standalone reusable skill |
| `/si:status` | Memory health dashboard — line counts, capacity, recommendations |
| `/si:remember` | Explicitly save important knowledge to auto-memory |
## Install
### Claude Code
```
/plugin marketplace add alirezarezvani/claude-skills
/plugin install self-improving-agent@claude-code-skills
```
### OpenClaw
```bash
clawhub install self-improving-agent
```
### Codex CLI
```bash
./scripts/codex-install.sh --skill self-improving-agent
```
## How It Works
```
Claude discovers pattern → auto-memory (MEMORY.md)
↓
Pattern recurs 2-3x → /si:review flags it
↓
You approve → /si:promote graduates it to CLAUDE.md
↓
Pattern becomes enforced rule, memory entry removed
↓
Space freed for new learnings
```
## What's Included
| Component | Count | Description |
|-----------|-------|-------------|
| Skills | 5 | review, promote, extract, status, remember |
| Agents | 2 | memory-analyst, skill-extractor |
| Hooks | 1 | PostToolUse error capture (zero overhead on success) |
| Reference docs | 3 | memory architecture, promotion rules, rules directory patterns |
| Templates | 2 | rule template, skill template |
## Design Principles
1. **Don't fight auto-memory — orchestrate it.** Auto-memory captures. This plugin curates.
2. **No duplicate storage.** Reads from `~/.claude/projects/` directly. No `.learnings/` directory.
3. **Zero capture overhead.** Auto-memory handles capture. Hook only fires on errors.
4. **Promotion = graduation.** Moving a pattern from MEMORY.md to CLAUDE.md changes its priority.
5. **Respect the 200-line limit.** Actively manages MEMORY.md capacity.
## Platform Support
| Platform | Memory System | Support |
|----------|--------------|---------|
| Claude Code | Auto-memory (MEMORY.md) | ✅ Full |
| OpenClaw | workspace/MEMORY.md | ✅ Adapted |
| Codex CLI | AGENTS.md | ✅ Adapted |
| GitHub Copilot | copilot-instructions.md | ⚠️ Manual |
## Credits
Inspired by [pskoett/self-improving-agent](https://clawhub.ai/pskoett/self-improving-agent) — a structured learning loop for AI coding agents. This plugin builds on that concept by integrating natively with Claude Code's auto-memory system.
## License
MIT — see [LICENSE](LICENSE)
FILE:agents/memory-analyst.md
# Memory Analyst Agent
You are a memory analyst for Claude Code projects. Your job is to analyze the auto-memory directory and produce actionable insights.
## Your Role
You analyze `~/.claude/projects/<project>/memory/` to find:
1. **Promotion candidates** — entries proven enough to become CLAUDE.md rules
2. **Stale entries** — references to files, tools, or patterns that no longer apply
3. **Consolidation opportunities** — multiple entries about the same topic
4. **Conflicts** — memory entries that contradict CLAUDE.md rules
5. **Health metrics** — capacity, freshness, organization
## Analysis Process
### 1. Read all memory files
- `MEMORY.md` (main file, first 200 lines loaded at startup)
- Any topic files (`debugging.md`, `patterns.md`, etc.)
- Note total line counts and file sizes
### 2. Cross-reference with CLAUDE.md
- Read `./CLAUDE.md` and `~/.claude/CLAUDE.md`
- Read all files in `.claude/rules/`
- Identify duplicates, contradictions, and gaps
### 3. Detect patterns
For each MEMORY.md entry, evaluate:
**Recurrence signals:**
- Same concept in multiple entries (paraphrased)
- Words like "again", "still", "always", "every time"
- Similar entries in topic files
**Staleness signals:**
- File paths that don't exist on disk (verify with `find` or `ls`)
- Version numbers that are outdated
- References to removed dependencies
- Patterns that contradict current CLAUDE.md
**Promotion signals:**
- Actionable (can be written as "Do X" / "Never Y")
- Broadly applicable (not a one-time debugging note)
- Not already in CLAUDE.md or rules/
- High impact (prevents common mistakes)
### 4. Score each entry
Rate each entry on three dimensions:
- **Durability** (0-3): Will this still be true in a month?
- **Impact** (0-3): How much does this affect daily work?
- **Scope** (0-3): Project-wide (3) vs. one-file (1) vs. one-time (0)
Promotion candidates: total score ≥ 6
### 5. Generate report
Organize findings into:
1. Promotion candidates (sorted by score, highest first)
2. Stale entries (with reason for staleness)
3. Consolidation groups (which entries to merge)
4. Conflicts (with both sides shown)
5. Health metrics (capacity, freshness)
6. Recommendations (top 3 actions)
## Output Format
Use the format defined in the `/si:review` skill. Be specific — include line numbers, exact text, and concrete suggestions.
## Constraints
- Never modify files directly — only analyze and report
- Don't invent entries — only report what's actually in the memory files
- Be concise — the report should be shorter than the memory files it analyzes
- Prioritize actionable findings over completeness
FILE:agents/skill-extractor.md
# Skill Extractor Agent
You are a skill extraction specialist. Your job is to transform proven patterns and debugging solutions into standalone, portable skills.
## Your Role
Given a pattern description (and optionally auto-memory entries), generate a complete skill package that:
- Solves a specific, recurring problem
- Works in any project (no hardcoded paths, credentials, or project-specific values)
- Is self-contained (readable without the original context)
- Follows the claude-skills format specification
## Extraction Process
### 1. Understand the pattern
From the input, identify:
- **The problem**: What goes wrong? What's the symptom?
- **The root cause**: Why does it happen?
- **The solution**: What's the fix? Are there multiple approaches?
- **The edge cases**: When does the solution NOT work?
- **The trigger conditions**: When should an agent use this skill?
### 2. Generate skill name
Rules:
- Lowercase, hyphens between words
- 2-4 words, descriptive
- Match the problem, not the project
- Examples: `docker-arm64-fixes`, `api-timeout-patterns`, `pnpm-monorepo-setup`
### 3. Create SKILL.md
Required structure:
```markdown
---
name: {{skill-name}}
description: "{{One sentence}}. Use when: {{trigger conditions}}."
---
# {{Skill Title}}
> {{One-line value proposition}}
## Quick Reference
| Problem | Solution |
|---------|----------|
| {{error/symptom}} | {{fix}} |
## The Problem
{{2-3 sentences. Include the error message or symptom people would search for.}}
## Solutions
### Option 1: {{Name}} (Recommended)
{{Step-by-step instructions with code blocks.}}
### Option 2: {{Alternative}} {{if applicable}}
{{When Option 1 doesn't apply.}}
## Trade-offs
| Approach | Pros | Cons |
|----------|------|------|
| {{option}} | {{pros}} | {{cons}} |
## Edge Cases
- {{When this approach breaks and what to do instead}}
## Related
- {{Links to official docs or related skills}}
```
### 4. Create README.md
Brief human-readable overview:
- What the skill does (1 paragraph)
- Installation instructions
- When to use it
- Credits/source
### 5. Quality checks
Before delivering, verify:
- [ ] YAML frontmatter is valid (`name` and `description` present)
- [ ] `name` in frontmatter matches folder name
- [ ] Description includes "Use when:" trigger
- [ ] No project-specific paths, URLs, or credentials
- [ ] Code examples are complete and runnable
- [ ] Error messages are exact (copy-pasteable for searching)
- [ ] Solutions work without additional context
- [ ] Trade-offs table helps users choose between options
- [ ] Skill is useful in a project you've never seen before
## Constraints
- **One problem per skill** — don't create omnibus guides
- **Show, don't tell** — code examples over prose
- **Include the error** — people search by error message
- **Be portable** — no `npm` vs `pnpm` assumptions
- **Keep it short** — under 200 lines for SKILL.md
- **No unnecessary files** — only SKILL.md is required. Add reference/ only if the topic is complex enough to warrant it
FILE:hooks/error-capture.sh
#!/bin/bash
# Self-Improving Agent — Error Capture Hook
# Fires on PostToolUse (Bash) to detect command failures.
# Zero output on success — only captures when errors are detected.
#
# Install: Add to .claude/settings.json:
# {
# "hooks": {
# "PostToolUse": [{
# "matcher": "Bash",
# "hooks": [{
# "type": "command",
# "command": "./skills/self-improving-agent/hooks/error-capture.sh"
# }]
# }]
# }
# }
set -e
OUTPUT="-"
# Exit silently if no output or empty
[ -z "$OUTPUT" ] && exit 0
# Error patterns — ordered by specificity
ERROR_PATTERNS=(
"error:"
"Error:"
"ERROR:"
"FATAL:"
"fatal:"
"FAILED"
"failed"
"command not found"
"No such file or directory"
"Permission denied"
"Module not found"
"ModuleNotFoundError"
"ImportError"
"SyntaxError"
"TypeError"
"ReferenceError"
"Cannot find module"
"ENOENT"
"EACCES"
"ECONNREFUSED"
"ETIMEDOUT"
"npm ERR!"
"pnpm ERR!"
"Traceback (most recent call last)"
"panic:"
"segmentation fault"
"core dumped"
"exit code"
"non-zero exit"
"Build failed"
"Compilation failed"
"Test failed"
)
# False positive exclusions — don't trigger on these
EXCLUSIONS=(
"error-capture" # Don't trigger on ourselves
"error_handler" # Code that handles errors
"errorHandler"
"error.log" # Log file references
"console.error" # Code that logs errors
"catch (error" # Error handling code
"catch (err"
".error(" # Logger calls
"no error" # Absence of error
"without error"
"error-free"
)
# Check exclusions first
for excl in "EXCLUSIONS[@]"; do
if [[ "$OUTPUT" == *"$excl"* ]]; then
exit 0
fi
done
# Check for error patterns
contains_error=false
matched_pattern=""
for pattern in "ERROR_PATTERNS[@]"; do
if [[ "$OUTPUT" == *"$pattern"* ]]; then
contains_error=true
matched_pattern="$pattern"
break
fi
done
# Exit silently if no error
[ "$contains_error" = false ] && exit 0
# Extract relevant error context (first 5 lines containing the pattern)
error_context=$(echo "$OUTPUT" | grep -i -m 5 "$matched_pattern" | head -5)
# Output a concise reminder — ~40 tokens
cat << EOF
<error-detected>
Command error detected (pattern: "$matched_pattern").
If this was unexpected or required investigation to fix, save the solution:
/si:remember "explanation of what went wrong and the fix"
Or if this is a known pattern, check: /si:review
Context: $(echo "$error_context" | head -2 | tr '\n' ' ' | cut -c1-200)
</error-detected>
EOF
FILE:hooks/hooks.json
{
"hooks": [
{
"name": "error-capture",
"event": "PostToolUse",
"matcher": "Bash",
"command": "./hooks/error-capture.sh",
"description": "Detects command failures and appends structured entries to auto-memory. Zero overhead on successful commands."
}
]
}
FILE:reference/memory-architecture.md
# Claude Code Memory Architecture
A complete reference for how Claude Code's memory systems work together.
## Three Memory Systems
### 1. CLAUDE.md Files (You → Claude)
**Purpose:** Persistent instructions you write to guide Claude's behavior.
**Locations (in priority order):**
| Scope | Path | Shared |
|-------|------|--------|
| Managed policy | `/etc/claude-code/CLAUDE.md` (Linux) | All users |
| Project | `./CLAUDE.md` or `./.claude/CLAUDE.md` | Team (git) |
| User | `~/.claude/CLAUDE.md` | Just you |
| Local | `./CLAUDE.local.md` | Just you |
**Loading:** Full file, every session. Files higher in the directory tree load first.
**Key facts:**
- Target under 200 lines per file
- Use `@path/to/file` syntax to import additional files (max 5 hops deep)
- More specific locations take precedence over broader ones
- Can import with `@README` or `@docs/guide.md`
- CLAUDE.local.md is auto-added to .gitignore
### 2. Auto Memory (Claude → Claude)
**Purpose:** Notes Claude writes to itself about project patterns and learnings.
**Location:** `~/.claude/projects/<project-path>/memory/`
**Structure:**
```
~/.claude/projects/<project-path>/memory/
├── MEMORY.md # Main file (first 200 lines loaded)
├── debugging.md # Topic file (loaded on demand)
├── patterns.md # Topic file (loaded on demand)
└── ... # More topic files as needed
```
**Key facts:**
- Enabled by default (since v2.1.32)
- Only the first 200 lines of MEMORY.md load at startup
- Claude creates topic files automatically when MEMORY.md gets long
- Git repo root determines the project path
- Git worktrees get separate memory directories
- Local only — not shared via git
- Toggle with `/memory`, settings, or `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1`
- Subagents can have their own auto memory
**What it captures:**
- Build commands and test conventions
- Debugging solutions and error patterns
- Code style preferences and architecture notes
- Your communication preferences and workflow habits
### 3. Session Memory (Claude → Claude)
**Purpose:** Conversation summaries for cross-session continuity.
**Location:** `~/.claude/projects/<project-path>/<session>/session-memory/`
**Key facts:**
- Saves what was discussed and decided in specific sessions
- "What did we do yesterday?" context
- Loaded contextually (relevant past sessions, not all)
- Use `/remember` to turn session memory into permanent project knowledge
### 4. Rules Directory (You → Claude, scoped)
**Purpose:** Modular instructions scoped to specific file types.
**Location:** `.claude/rules/*.md`
**Key facts:**
- Uses YAML frontmatter with `paths` field for scoping
- Only loads when Claude works with matching files
- Recursive — can organize into subdirectories
- Same priority as `.claude/CLAUDE.md`
- Great for keeping CLAUDE.md under 200 lines
```yaml
---
paths:
- "src/api/**/*.ts"
---
# API rules only load when working with API files
```
## Memory Priority
When entries conflict:
1. CLAUDE.md (highest — explicit instructions)
2. `.claude/rules/` (high — scoped instructions)
3. Auto-memory MEMORY.md (medium — learned patterns)
4. Session memory (low — historical context)
## The Self-Improving Agent's Role
```
Auto-memory captures → This plugin curates → CLAUDE.md enforces
MEMORY.md (raw notes) → /si:review (analyze) → /si:promote (graduate)
↓
CLAUDE.md or
.claude/rules/
(enforced rules)
```
**Why this matters:** MEMORY.md entries are background context truncated at 200 lines. CLAUDE.md entries are high-priority instructions loaded in full. Promoting a pattern from memory to rules fundamentally changes how Claude treats it.
## Capacity Planning
| File | Soft limit | Hard limit | What happens at limit |
|------|-----------|------------|----------------------|
| MEMORY.md | 150 lines | 200 lines | Lines after 200 not loaded at startup |
| CLAUDE.md | 150 lines | No hard limit | Adherence decreases with length |
| Topic files | No limit | No limit | Loaded on demand, not at startup |
| Rules files | No limit per file | No limit | Only loaded when paths match |
## Best Practices
1. **Keep MEMORY.md lean** — promote proven patterns, delete stale ones
2. **Keep CLAUDE.md under 200 lines** — split into rules/ if growing
3. **Don't duplicate** — if it's in CLAUDE.md, remove it from MEMORY.md
4. **Scope rules** — use `.claude/rules/` with paths for file-type-specific patterns
5. **Review quarterly** — memory files go stale after refactors
6. **Use /si:status** — monitor capacity before it becomes a problem
FILE:reference/promotion-rules.md
# Promotion Rules
When to promote a learning from auto-memory (MEMORY.md) to the project's rule system (CLAUDE.md or `.claude/rules/`).
## Promotion Criteria
A learning should be promoted when **all three** are true:
1. **Proven** — appeared in 2+ sessions or confirmed correct after testing
2. **Actionable** — can be written as a concrete instruction ("Use X", "Never Y")
3. **Durable** — will still be true in 30+ days
## Scoring Guide
| Dimension | Score 0 | Score 1 | Score 2 | Score 3 |
|-----------|---------|---------|---------|---------|
| **Durability** | One-time fix | Temporary workaround | Stable pattern | Architectural truth |
| **Impact** | Nice-to-know | Saves 1 minute | Prevents mistakes | Prevents breakage |
| **Scope** | One file only | One directory | Entire project | All your projects |
**Promote when total ≥ 6.** Watch when total = 4-5. Ignore when total ≤ 3.
## Target Selection
### Use CLAUDE.md when:
- The rule applies to the entire project
- It's a build command, test convention, or architecture decision
- Any contributor (human or AI) needs to know it
- It's short enough to add without exceeding 200 lines
### Use .claude/rules/ when:
- The rule only applies to specific file types
- CLAUDE.md is already near 200 lines
- The rule needs detailed explanation (multiple paragraphs)
- You want it to load only when relevant files are open
### Use ~/.claude/CLAUDE.md when:
- The rule applies to all your projects
- It's a personal preference, not a project convention
- Examples: "Prefer explicit returns over implicit", "Use descriptive variable names"
## Distillation Rules
When promoting, transform the learning:
### From descriptive to prescriptive
❌ "I noticed the project uses pnpm workspaces. npm install fails because of the lock file."
✅ "Use `pnpm install`, not npm. Lock file: `pnpm-lock.yaml`."
### From verbose to concise
❌ "When modifying API endpoints in the OpenAPI spec file, you need to regenerate the TypeScript client by running the generate command, otherwise the types won't match at runtime and you'll get errors."
✅ "After editing `openapi.yaml`: run `pnpm run generate:api` to regenerate TS client."
### From conditional to absolute
❌ "Sometimes you need to restart the dev server after changing environment variables."
✅ "Restart dev server after any `.env` change — hot reload doesn't pick up env vars."
## Anti-Patterns
### Don't promote:
- **One-time debugging notes** — "Fixed the CORS issue by adding header X" (unless it recurs)
- **Session-specific context** — "We decided to use Approach A in today's meeting"
- **Unstable patterns** — "Currently using v3 of the API" (will change)
- **Obvious things** — "Run tests before committing" (Claude knows this)
- **Credentials or secrets** — never store in any memory file
### Don't duplicate:
- If CLAUDE.md already says "Use pnpm", don't also keep it in MEMORY.md
- After promoting, remove the source entry to free space
## Promotion Workflow
```
1. /si:review identifies candidate
2. Confirm the pattern is still valid
3. Distill into one-line instruction
4. /si:promote writes to CLAUDE.md or rules/
5. Remove from MEMORY.md
6. Verify with /si:status
```
FILE:reference/rules-directory-patterns.md
# Rules Directory Patterns
Best practices for organizing `.claude/rules/` files — the scoped instruction system that loads rules only when relevant files are open.
## Directory Structure
```
.claude/
├── CLAUDE.md # Main project instructions (always loaded)
└── rules/
├── code-style.md # No paths → loads always (like CLAUDE.md)
├── testing.md # Scoped to test files
├── api-design.md # Scoped to API source files
├── database.md # Scoped to migration/model files
└── frontend/
├── components.md # Scoped to React components
└── styling.md # Scoped to CSS/styled files
```
## Path Scoping
### Basic patterns
```yaml
---
paths:
- "**/*.test.ts" # All TypeScript test files
- "src/api/**/*.ts" # API source files
- "*.md" # Root-level markdown
- "src/components/**/*.tsx" # React components
---
```
### Brace expansion
```yaml
---
paths:
- "src/**/*.{ts,tsx}" # All TypeScript + TSX
- "tests/**/*.{test,spec}.ts" # Test and spec files
---
```
### Multiple scopes
```yaml
---
paths:
- "src/api/**/*.ts"
- "tests/api/**/*"
- "openapi.yaml"
---
```
## Common Rule Files
### testing.md
```yaml
---
paths:
- "**/*.test.{ts,tsx,js,jsx}"
- "**/*.spec.{ts,tsx,js,jsx}"
- "tests/**/*"
- "__tests__/**/*"
---
# Testing Rules
- Use `describe` blocks to group related tests
- One assertion per test when possible
- Mock external services; never hit real APIs in tests
- Use factories for test data, not inline objects
- Run `pnpm test` before committing
```
### api-design.md
```yaml
---
paths:
- "src/api/**/*.ts"
- "src/routes/**/*.ts"
- "src/handlers/**/*.ts"
---
# API Design Rules
- Validate all input with Zod schemas
- Use `ApiError` class for error responses
- Include OpenAPI JSDoc on all handlers
- Return consistent error format: `{ error: string, code: string }`
```
### database.md
```yaml
---
paths:
- "src/db/**/*"
- "migrations/**/*"
- "prisma/**/*"
- "drizzle/**/*"
---
# Database Rules
- Always create a migration for schema changes
- Never modify existing migrations — create new ones
- Use transactions for multi-table operations
- Index foreign keys and frequently queried columns
```
### security.md (unscoped — always loads)
```markdown
# Security Rules
- Never log sensitive data (tokens, passwords, PII)
- Sanitize all user input before database queries
- Use parameterized queries, never string interpolation
- Validate file uploads: type, size, content
- Environment variables for all secrets — never hardcode
```
## When to Create a Rule File
| Signal | Action |
|--------|--------|
| CLAUDE.md over 150 lines | Move scoped patterns to rules/ |
| Same instruction repeated for different file types | Create a scoped rule |
| `/si:promote` suggests a file-type-specific pattern | Create or append to a rule file |
| Team adds a new convention for a specific area | New rule file |
## Organization Tips
1. **One topic per file** — `testing.md`, not `testing-and-linting.md`
2. **Use subdirectories for large projects** — `rules/frontend/`, `rules/backend/`
3. **Keep unscoped rules minimal** — they load every session like CLAUDE.md
4. **Review after refactors** — paths may change when directories are reorganized
5. **Share via git** — rules/ should be version-controlled (unlike auto-memory)
FILE:settings.json
{
"name": "self-improving-agent",
"displayName": "Self-Improving Agent",
"version": "1.0.0",
"description": "Curate auto-memory, promote learnings to rules, extract skills from patterns.",
"author": "Reza Rezvani",
"license": "MIT",
"platforms": ["claude-code", "openclaw", "codex"],
"category": "development",
"tags": ["memory", "auto-memory", "self-improvement", "learning", "rules", "skills"],
"repository": "https://github.com/alirezarezvani/claude-skills",
"commands": {
"review": "/si:review",
"promote": "/si:promote",
"extract": "/si:extract",
"status": "/si:status",
"remember": "/si:remember"
},
"hooks": {
"PostToolUse": {
"Bash": "hooks/error-capture.sh"
}
},
"agents": [
"memory-analyst",
"skill-extractor"
]
}
FILE:skills/extract/SKILL.md
---
name: extract
description: "Turn a proven pattern or debugging solution into a standalone reusable skill with SKILL.md, reference docs, and examples."
command: /si:extract
---
# /si:extract — Create Skills from Patterns
Transforms a recurring pattern or debugging solution into a standalone, portable skill that can be installed in any project.
## Usage
```
/si:extract <pattern description> # Interactive extraction
/si:extract <pattern> --name docker-m1-fixes # Specify skill name
/si:extract <pattern> --output ./skills/ # Custom output directory
/si:extract <pattern> --dry-run # Preview without creating files
```
## When to Extract
A learning qualifies for skill extraction when ANY of these are true:
| Criterion | Signal |
|---|---|
| **Recurring** | Same issue across 2+ projects |
| **Non-obvious** | Required real debugging to discover |
| **Broadly applicable** | Not tied to one specific codebase |
| **Complex solution** | Multi-step fix that's easy to forget |
| **User-flagged** | "Save this as a skill", "I want to reuse this" |
## Workflow
### Step 1: Identify the pattern
Read the user's description. Search auto-memory for related entries:
```bash
MEMORY_DIR="$HOME/.claude/projects/$(pwd | sed 's|/|%2F|g; s|%2F|/|; s|^/||')/memory"
grep -rni "<keywords>" "$MEMORY_DIR/"
```
If found in auto-memory, use those entries as source material. If not, use the user's description directly.
### Step 2: Determine skill scope
Ask (max 2 questions):
- "What problem does this solve?" (if not clear)
- "Should this include code examples?" (if applicable)
### Step 3: Generate skill name
Rules for naming:
- Lowercase, hyphens between words
- Descriptive but concise (2-4 words)
- Examples: `docker-m1-fixes`, `api-timeout-patterns`, `pnpm-workspace-setup`
### Step 4: Create the skill files
**Spawn the `skill-extractor` agent** for the actual file generation.
The agent creates:
```
<skill-name>/
├── SKILL.md # Main skill file with frontmatter
├── README.md # Human-readable overview
└── reference/ # (optional) Supporting documentation
└── examples.md # Concrete examples and edge cases
```
### Step 5: SKILL.md structure
The generated SKILL.md must follow this format:
```markdown
---
name: <skill-name>
description: "<one-line description>. Use when: <trigger conditions>."
---
# <Skill Title>
> One-line summary of what this skill solves.
## Quick Reference
| Problem | Solution |
|---------|----------|
| {{problem 1}} | {{solution 1}} |
| {{problem 2}} | {{solution 2}} |
## The Problem
{{2-3 sentences explaining what goes wrong and why it's non-obvious.}}
## Solutions
### Option 1: {{Name}} (Recommended)
{{Step-by-step with code examples.}}
### Option 2: {{Alternative}}
{{For when Option 1 doesn't apply.}}
## Trade-offs
| Approach | Pros | Cons |
|----------|------|------|
| Option 1 | {{pros}} | {{cons}} |
| Option 2 | {{pros}} | {{cons}} |
## Edge Cases
- {{edge case 1 and how to handle it}}
- {{edge case 2 and how to handle it}}
```
### Step 6: Quality gates
Before finalizing, verify:
- [ ] SKILL.md has valid YAML frontmatter with `name` and `description`
- [ ] `name` matches the folder name (lowercase, hyphens)
- [ ] Description includes "Use when:" trigger conditions
- [ ] Solutions are self-contained (no external context needed)
- [ ] Code examples are complete and copy-pasteable
- [ ] No project-specific hardcoded values (paths, URLs, credentials)
- [ ] No unnecessary dependencies
### Step 7: Report
```
✅ Skill extracted: {{skill-name}}
Files created:
{{path}}/SKILL.md ({{lines}} lines)
{{path}}/README.md ({{lines}} lines)
{{path}}/reference/examples.md ({{lines}} lines)
Install: /plugin install (copy to your skills directory)
Publish: clawhub publish {{path}}
Source: MEMORY.md entries at lines {{n, m, ...}} (retained — the skill is portable, the memory is project-specific)
```
## Examples
### Extracting a debugging pattern
```
/si:extract "Fix for Docker builds failing on Apple Silicon with platform mismatch"
```
Creates `docker-m1-fixes/SKILL.md` with:
- The platform mismatch error message
- Three solutions (build flag, Dockerfile, docker-compose)
- Trade-offs table
- Performance note about Rosetta 2 emulation
### Extracting a workflow pattern
```
/si:extract "Always regenerate TypeScript API client after modifying OpenAPI spec"
```
Creates `api-client-regen/SKILL.md` with:
- Why manual regen is needed
- The exact command sequence
- CI integration snippet
- Common failure modes
## Tips
- Extract patterns that would save time in a *different* project
- Keep skills focused — one problem per skill
- Include the error messages people would search for
- Test the skill by reading it without the original context — does it make sense?
FILE:skills/promote/SKILL.md
---
name: promote
description: "Graduate a proven pattern from auto-memory (MEMORY.md) to CLAUDE.md or .claude/rules/ for permanent enforcement."
command: /si:promote
---
# /si:promote — Graduate Learnings to Rules
Moves a proven pattern from Claude's auto-memory into the project's rule system, where it becomes an enforced instruction rather than a background note.
## Usage
```
/si:promote <pattern description> # Auto-detect best target
/si:promote <pattern> --target claude.md # Promote to CLAUDE.md
/si:promote <pattern> --target rules/testing.md # Promote to scoped rule
/si:promote <pattern> --target rules/api.md --paths "src/api/**/*.ts" # Scoped with paths
```
## Workflow
### Step 1: Understand the pattern
Parse the user's description. If vague, ask one clarifying question:
- "What specific behavior should Claude follow?"
- "Does this apply to all files or specific paths?"
### Step 2: Find the pattern in auto-memory
```bash
# Search MEMORY.md for related entries
MEMORY_DIR="$HOME/.claude/projects/$(pwd | sed 's|/|%2F|g; s|%2F|/|; s|^/||')/memory"
grep -ni "<keywords>" "$MEMORY_DIR/MEMORY.md"
```
Show the matching entries and confirm they're what the user means.
### Step 3: Determine the right target
| Pattern scope | Target | Example |
|---|---|---|
| Applies to entire project | `./CLAUDE.md` | "Use pnpm, not npm" |
| Applies to specific file types | `.claude/rules/<topic>.md` | "API handlers need validation" |
| Applies to all your projects | `~/.claude/CLAUDE.md` | "Prefer explicit error handling" |
If the user didn't specify a target, recommend one based on scope.
### Step 4: Distill into a concise rule
Transform the learning from auto-memory's note format into CLAUDE.md's instruction format:
**Before** (MEMORY.md — descriptive):
> The project uses pnpm workspaces. When I tried npm install it failed. The lock file is pnpm-lock.yaml. Must use pnpm install for dependencies.
**After** (CLAUDE.md — prescriptive):
```markdown
## Build & Dependencies
- Package manager: pnpm (not npm). Use `pnpm install`.
```
**Rules for distillation:**
- One line per rule when possible
- Imperative voice ("Use X", "Always Y", "Never Z")
- Include the command or example, not just the concept
- No backstory — just the instruction
### Step 5: Write to target
**For CLAUDE.md:**
1. Read existing CLAUDE.md
2. Find the appropriate section (or create one)
3. Append the new rule under the right heading
4. If file would exceed 200 lines, suggest using `.claude/rules/` instead
**For `.claude/rules/`:**
1. Create the file if it doesn't exist
2. Add YAML frontmatter with `paths` if scoped
3. Write the rule content
```markdown
---
paths:
- "src/api/**/*.ts"
- "tests/api/**/*"
---
# API Development Rules
- All endpoints must validate input with Zod schemas
- Use `ApiError` class for error responses (not raw Error)
- Include OpenAPI JSDoc comments on handler functions
```
### Step 6: Clean up auto-memory
After promoting, remove or mark the original entry in MEMORY.md:
```bash
# Show what will be removed
grep -n "<pattern>" "$MEMORY_DIR/MEMORY.md"
```
Ask the user to confirm removal. Then edit MEMORY.md to remove the promoted entry. This frees space for new learnings.
### Step 7: Confirm
```
✅ Promoted to {{target}}
Rule: "{{distilled rule}}"
Source: MEMORY.md line {{n}} (removed)
MEMORY.md: {{lines}}/200 lines remaining
The pattern is now an enforced instruction. Claude will follow it in all future sessions.
```
## Promotion Decision Guide
### Promote when:
- Pattern appeared 3+ times in auto-memory
- You corrected Claude about it more than once
- It's a project convention that any contributor should know
- It prevents a recurring mistake
### Don't promote when:
- It's a one-time debugging note (leave in auto-memory)
- It's session-specific context (session memory handles this)
- It might change soon (e.g., during a migration)
- It's already covered by existing rules
### CLAUDE.md vs .claude/rules/
| Use CLAUDE.md for | Use .claude/rules/ for |
|---|---|
| Global project rules | File-type-specific patterns |
| Build commands | Testing conventions |
| Architecture decisions | API design rules |
| Team conventions | Framework-specific gotchas |
## Tips
- Keep CLAUDE.md under 200 lines — use rules/ for overflow
- One rule per line is easier to maintain than paragraphs
- Include the concrete command, not just the concept
- Review promoted rules quarterly — remove what's no longer relevant
FILE:skills/remember/SKILL.md
---
name: remember
description: "Explicitly save important knowledge to auto-memory with timestamp and context. Use when a discovery is too important to rely on auto-capture."
command: /si:remember
---
# /si:remember — Save Knowledge Explicitly
Writes an explicit entry to auto-memory when something is important enough that you don't want to rely on Claude noticing it automatically.
## Usage
```
/si:remember <what to remember>
/si:remember "This project's CI requires Node 20 LTS — v22 breaks the build"
/si:remember "The /api/auth endpoint uses a custom JWT library, not passport"
/si:remember "Reza prefers explicit error handling over try-catch-all patterns"
```
## When to Use
| Situation | Example |
|-----------|---------|
| Hard-won debugging insight | "CORS errors on /api/upload are caused by the CDN, not the backend" |
| Project convention not in CLAUDE.md | "We use barrel exports in src/components/" |
| Tool-specific gotcha | "Jest needs `--forceExit` flag or it hangs on DB tests" |
| Architecture decision | "We chose Drizzle over Prisma for type-safe SQL" |
| Preference you want Claude to learn | "Don't add comments explaining obvious code" |
## Workflow
### Step 1: Parse the knowledge
Extract from the user's input:
- **What**: The concrete fact or pattern
- **Why it matters**: Context (if provided)
- **Scope**: Project-specific or global?
### Step 2: Check for duplicates
```bash
MEMORY_DIR="$HOME/.claude/projects/$(pwd | sed 's|/|%2F|g; s|%2F|/|; s|^/||')/memory"
grep -ni "<keywords>" "$MEMORY_DIR/MEMORY.md" 2>/dev/null
```
If a similar entry exists:
- Show it to the user
- Ask: "Update the existing entry or add a new one?"
### Step 3: Write to MEMORY.md
Append to the end of `MEMORY.md`:
```markdown
- {{concise fact or pattern}}
```
Keep entries concise — one line when possible. Auto-memory entries don't need timestamps, IDs, or metadata. They're notes, not database records.
If MEMORY.md is over 180 lines, warn the user:
```
⚠️ MEMORY.md is at {{n}}/200 lines. Consider running /si:review to free space.
```
### Step 4: Suggest promotion
If the knowledge sounds like a rule (imperative, always/never, convention):
```
💡 This sounds like it could be a CLAUDE.md rule rather than a memory entry.
Rules are enforced with higher priority. Want to /si:promote it instead?
```
### Step 5: Confirm
```
✅ Saved to auto-memory
"{{entry}}"
MEMORY.md: {{n}}/200 lines
Claude will see this at the start of every session in this project.
```
## What NOT to use /si:remember for
- **Temporary context**: Use session memory or just tell Claude in conversation
- **Enforced rules**: Use `/si:promote` to write directly to CLAUDE.md
- **Cross-project knowledge**: Use `~/.claude/CLAUDE.md` for global rules
- **Sensitive data**: Never store credentials, tokens, or secrets in memory files
## Tips
- Be concise — one line beats a paragraph
- Include the concrete command or value, not just the concept
- ✅ "Build with `pnpm build`, tests with `pnpm test:e2e`"
- ❌ "The project uses pnpm for building and testing"
- If you're remembering the same thing twice, promote it to CLAUDE.md
FILE:skills/review/SKILL.md
---
name: review
description: "Analyze auto-memory for promotion candidates, stale entries, consolidation opportunities, and health metrics."
command: /si:review
---
# /si:review — Analyze Auto-Memory
Performs a comprehensive audit of Claude Code's auto-memory and produces actionable recommendations.
## Usage
```
/si:review # Full review
/si:review --quick # Summary only (counts + top 3 candidates)
/si:review --stale # Focus on stale/outdated entries
/si:review --candidates # Show only promotion candidates
```
## What It Does
### Step 1: Locate memory directory
```bash
# Find the project's auto-memory directory
MEMORY_DIR="$HOME/.claude/projects/$(pwd | sed 's|/|%2F|g; s|%2F|/|; s|^/||')/memory"
# Fallback: check common path patterns
# ~/.claude/projects/<user>/<project>/memory/
# ~/.claude/projects/<absolute-path>/memory/
# List all memory files
ls -la "$MEMORY_DIR"/
```
If memory directory doesn't exist, report that auto-memory may be disabled. Suggest checking with `/memory`.
### Step 2: Read and analyze MEMORY.md
Read the full `MEMORY.md` file. Count lines and check against the 200-line startup limit.
Analyze each entry for:
1. **Recurrence indicators**
- Same concept appears multiple times (different wording)
- References to "again" or "still" or "keeps happening"
- Similar entries across topic files
2. **Staleness indicators**
- References files that no longer exist (`find` to verify)
- Mentions outdated tools, versions, or commands
- Contradicts current CLAUDE.md rules
3. **Consolidation opportunities**
- Multiple entries about the same topic (e.g., three lines about testing)
- Entries that could merge into one concise rule
4. **Promotion candidates** — entries that meet ALL criteria:
- Appeared in 2+ sessions (check wording patterns)
- Not project-specific trivia (broadly useful)
- Actionable (can be written as a concrete rule)
- Not already in CLAUDE.md or `.claude/rules/`
### Step 3: Read topic files
If `MEMORY.md` references or the directory contains additional files (`debugging.md`, `patterns.md`, etc.):
- Read each one
- Cross-reference with MEMORY.md for duplicates
- Check for entries that belong in the main file (high value) vs. topic files (details)
### Step 4: Cross-reference with CLAUDE.md
Read the project's `CLAUDE.md` (if it exists) and compare:
- Are there MEMORY.md entries that duplicate CLAUDE.md rules? (→ remove from memory)
- Are there MEMORY.md entries that contradict CLAUDE.md? (→ flag conflict)
- Are there MEMORY.md patterns not yet in CLAUDE.md that should be? (→ promotion candidate)
Also check `.claude/rules/` directory for existing scoped rules.
### Step 5: Generate report
Output format:
```
📊 Auto-Memory Review
Memory Health:
MEMORY.md: {{lines}}/200 lines ({{percent}}%)
Topic files: {{count}} ({{names}})
CLAUDE.md: {{lines}} lines
Rules: {{count}} files in .claude/rules/
🎯 Promotion Candidates ({{count}}):
1. "{{pattern}}" — seen {{n}}x, applies broadly
→ Suggest: {{target}} (CLAUDE.md / .claude/rules/{{name}}.md)
2. ...
🗑️ Stale Entries ({{count}}):
1. Line {{n}}: "{{entry}}" — {{reason}}
2. ...
🔄 Consolidation ({{count}} groups):
1. Lines {{a}}, {{b}}, {{c}} all about {{topic}} → merge into 1 entry
2. ...
⚠️ Conflicts ({{count}}):
1. MEMORY.md line {{n}} contradicts CLAUDE.md: {{detail}}
💡 Recommendations:
- {{actionable suggestion}}
- {{actionable suggestion}}
```
## When to Use
- After completing a major feature or debugging session
- When `/si:status` shows MEMORY.md is over 150 lines
- Weekly during active development
- Before starting a new project phase
- After onboarding a new team member (review what Claude learned)
## Tips
- Run `/si:review --quick` frequently (low overhead)
- Full review is most valuable when MEMORY.md is getting crowded
- Act on promotion candidates promptly — they're proven patterns
- Don't hesitate to delete stale entries — auto-memory will re-learn if needed
FILE:skills/status/SKILL.md
---
name: status
description: "Memory health dashboard showing line counts, topic files, capacity, stale entries, and recommendations."
command: /si:status
---
# /si:status — Memory Health Dashboard
Quick overview of your project's memory state across all memory systems.
## Usage
```
/si:status # Full dashboard
/si:status --brief # One-line summary
```
## What It Reports
### Step 1: Locate all memory files
```bash
# Auto-memory directory
MEMORY_DIR="$HOME/.claude/projects/$(pwd | sed 's|/|%2F|g; s|%2F|/|; s|^/||')/memory"
# Count lines in MEMORY.md
wc -l "$MEMORY_DIR/MEMORY.md" 2>/dev/null || echo "0"
# List topic files
ls "$MEMORY_DIR/"*.md 2>/dev/null | grep -v MEMORY.md
# CLAUDE.md
wc -l ./CLAUDE.md 2>/dev/null || echo "0"
wc -l ~/.claude/CLAUDE.md 2>/dev/null || echo "0"
# Rules directory
ls .claude/rules/*.md 2>/dev/null | wc -l
```
### Step 2: Analyze capacity
| Metric | Healthy | Warning | Critical |
|--------|---------|---------|----------|
| MEMORY.md lines | < 120 | 120-180 | > 180 |
| CLAUDE.md lines | < 150 | 150-200 | > 200 |
| Topic files | 0-3 | 4-6 | > 6 |
| Stale entries | 0 | 1-3 | > 3 |
### Step 3: Quick stale check
For each MEMORY.md entry that references a file path:
```bash
# Verify referenced files still exist
grep -oE '[a-zA-Z0-9_/.-]+\.(ts|js|py|md|json|yaml|yml)' "$MEMORY_DIR/MEMORY.md" | while read f; do
[ ! -f "$f" ] && echo "STALE: $f"
done
```
### Step 4: Output
```
📊 Memory Status
Auto-Memory (MEMORY.md):
Lines: {{n}}/200 ({{bar}}) {{emoji}}
Topic files: {{count}} ({{names}})
Last updated: {{date}}
Project Rules:
CLAUDE.md: {{n}} lines
Rules: {{count}} files in .claude/rules/
User global: {{n}} lines (~/.claude/CLAUDE.md)
Health:
Capacity: {{healthy/warning/critical}}
Stale refs: {{count}} (files no longer exist)
Duplicates: {{count}} (entries repeated across files)
{{if recommendations}}
💡 Recommendations:
- {{recommendation}}
{{endif}}
```
### Brief mode
```
/si:status --brief
```
Output: `📊 Memory: {{n}}/200 lines | {{count}} rules | {{status_emoji}} {{status_word}}`
## Interpretation
- **Green (< 60%)**: Plenty of room. Auto-memory is working well.
- **Yellow (60-90%)**: Getting full. Consider running `/si:review` to promote or clean up.
- **Red (> 90%)**: Near capacity. Auto-memory may start dropping older entries. Run `/si:review` now.
## Tips
- Run `/si:status --brief` as a quick check anytime
- If capacity is yellow+, run `/si:review` to identify promotion candidates
- Stale entries waste space — delete references to files that no longer exist
- Topic files are fine — Claude creates them to keep MEMORY.md under 200 lines
FILE:templates/rule-template.md
---
paths:
- "{{glob-pattern}}"
---
# {{Topic}} Rules
## Conventions
- {{convention 1}}
- {{convention 2}}
## Patterns
- {{preferred pattern with example}}
- {{anti-pattern to avoid}}
## Commands
- {{relevant command}}: `{{command}}`
FILE:templates/skill-template.md
---
name: {{skill-name}}
description: "{{One-line description}}. Use when: {{trigger conditions}}."
---
# {{Skill Title}}
> {{One-line value proposition}}
## Quick Reference
| Problem | Solution |
|---------|----------|
| {{error/symptom 1}} | {{fix 1}} |
| {{error/symptom 2}} | {{fix 2}} |
## The Problem
{{2-3 sentences explaining what goes wrong and why.
Include the exact error message if applicable.}}
## Solutions
### Option 1: {{Name}} (Recommended)
{{Step-by-step instructions.}}
```{{language}}
{{code example}}
```
### Option 2: {{Alternative}}
{{When Option 1 doesn't apply.}}
```{{language}}
{{code example}}
```
## Trade-offs
| Approach | Pros | Cons |
|----------|------|------|
| Option 1 | {{pros}} | {{cons}} |
| Option 2 | {{pros}} | {{cons}} |
## Edge Cases
- {{edge case and how to handle it}}
## Related
- {{link to official docs}}
Production-grade Playwright testing toolkit. Use when the user mentions Playwright tests, end-to-end testing, browser automation, fixing flaky tests, test mi...
---
name: "playwright-pro"
description: "Production-grade Playwright testing toolkit. Use when the user mentions Playwright tests, end-to-end testing, browser automation, fixing flaky tests, test migration, CI/CD testing, or test suites. Generate tests, fix flaky failures, migrate from Cypress/Selenium, sync with TestRail, run on BrowserStack. 55 templates, 3 agents, smart reporting."
---
# Playwright Pro
Production-grade Playwright testing toolkit for AI coding agents.
## Available Commands
When installed as a Claude Code plugin, these are available as `/pw:` commands:
| Command | What it does |
|---|---|
| `/pw:init` | Set up Playwright — detects framework, generates config, CI, first test |
| `/pw:generate <spec>` | Generate tests from user story, URL, or component |
| `/pw:review` | Review tests for anti-patterns and coverage gaps |
| `/pw:fix <test>` | Diagnose and fix failing or flaky tests |
| `/pw:migrate` | Migrate from Cypress or Selenium to Playwright |
| `/pw:coverage` | Analyze what's tested vs. what's missing |
| `/pw:testrail` | Sync with TestRail — read cases, push results |
| `/pw:browserstack` | Run on BrowserStack, pull cross-browser reports |
| `/pw:report` | Generate test report in your preferred format |
## Quick Start Workflow
The recommended sequence for most projects:
```
1. /pw:init → scaffolds config, CI pipeline, and a first smoke test
2. /pw:generate → generates tests from your spec or URL
3. /pw:review → validates quality and flags anti-patterns ← always run after generate
4. /pw:fix <test> → diagnoses and repairs any failing/flaky tests ← run when CI turns red
```
**Validation checkpoints:**
- After `/pw:generate` — always run `/pw:review` before committing; it catches locator anti-patterns and missing assertions automatically.
- After `/pw:fix` — re-run the full suite locally (`npx playwright test`) to confirm the fix doesn't introduce regressions.
- After `/pw:migrate` — run `/pw:coverage` to confirm parity with the old suite before decommissioning Cypress/Selenium tests.
### Example: Generate → Review → Fix
```bash
# 1. Generate tests from a user story
/pw:generate "As a user I can log in with email and password"
# Generated: tests/auth/login.spec.ts
# → Playwright Pro creates the file using the auth template.
# 2. Review the generated tests
/pw:review tests/auth/login.spec.ts
# → Flags: one test used page.locator('input[type=password]') — suggests getByLabel('Password')
# → Fix applied automatically.
# 3. Run locally to confirm
npx playwright test tests/auth/login.spec.ts --headed
# 4. If a test is flaky in CI, diagnose it
/pw:fix tests/auth/login.spec.ts
# → Identifies missing web-first assertion; replaces waitForTimeout(2000) with expect(locator).toBeVisible()
```
## Golden Rules
1. `getByRole()` over CSS/XPath — resilient to markup changes
2. Never `page.waitForTimeout()` — use web-first assertions
3. `expect(locator)` auto-retries; `expect(await locator.textContent())` does not
4. Isolate every test — no shared state between tests
5. `baseURL` in config — zero hardcoded URLs
6. Retries: `2` in CI, `0` locally
7. Traces: `'on-first-retry'` — rich debugging without slowdown
8. Fixtures over globals — `test.extend()` for shared state
9. One behavior per test — multiple related assertions are fine
10. Mock external services only — never mock your own app
## Locator Priority
```
1. getByRole() — buttons, links, headings, form elements
2. getByLabel() — form fields with labels
3. getByText() — non-interactive text
4. getByPlaceholder() — inputs with placeholder
5. getByTestId() — when no semantic option exists
6. page.locator() — CSS/XPath as last resort
```
## What's Included
- **9 skills** with detailed step-by-step instructions
- **3 specialized agents**: test-architect, test-debugger, migration-planner
- **55 test templates**: auth, CRUD, checkout, search, forms, dashboard, settings, onboarding, notifications, API, accessibility
- **2 MCP servers** (TypeScript): TestRail and BrowserStack integrations
- **Smart hooks**: auto-validate test quality, auto-detect Playwright projects
- **6 reference docs**: golden rules, locators, assertions, fixtures, pitfalls, flaky tests
- **Migration guides**: Cypress and Selenium mapping tables
## Integration Setup
### TestRail (Optional)
```bash
export TESTRAIL_URL="https://your-instance.testrail.io"
export TESTRAIL_USER="[email protected]"
export TESTRAIL_API_KEY="your-api-key"
```
### BrowserStack (Optional)
```bash
export BROWSERSTACK_USERNAME="your-username"
export BROWSERSTACK_ACCESS_KEY="your-access-key"
```
## Quick Reference
See `reference/` directory for:
- `golden-rules.md` — The 10 non-negotiable rules
- `locators.md` — Complete locator priority with cheat sheet
- `assertions.md` — Web-first assertions reference
- `fixtures.md` — Custom fixtures and storageState patterns
- `common-pitfalls.md` — Top 10 mistakes and fixes
- `flaky-tests.md` — Diagnosis commands and quick fixes
See `templates/README.md` for the full template index.
FILE:CLAUDE.md
# Playwright Pro — Agent Context
You are working in a project with the Playwright Pro plugin installed. Follow these rules for all test-related work.
## Golden Rules (Non-Negotiable)
1. **`getByRole()` over CSS/XPath** — resilient to markup changes, mirrors how users see the page
2. **Never `page.waitForTimeout()`** — use `expect(locator).toBeVisible()` or `page.waitForURL()`
3. **Web-first assertions** — `expect(locator)` auto-retries; `expect(await locator.textContent())` does not
4. **Isolate every test** — no shared state, no execution-order dependencies
5. **`baseURL` in config** — zero hardcoded URLs in tests
6. **Retries: `2` in CI, `0` locally** — surface flakiness where it matters
7. **Traces: `'on-first-retry'`** — rich debugging without CI slowdown
8. **Fixtures over globals** — share state via `test.extend()`, not module-level variables
9. **One behavior per test** — multiple related `expect()` calls are fine
10. **Mock external services only** — never mock your own app
## Locator Priority
Always use the first option that works:
```typescript
page.getByRole('button', { name: 'Submit' }) // 1. Role (default)
page.getByLabel('Email address') // 2. Label (form fields)
page.getByText('Welcome back') // 3. Text (non-interactive)
page.getByPlaceholder('Search...') // 4. Placeholder
page.getByAltText('Company logo') // 5. Alt text (images)
page.getByTitle('Close dialog') // 6. Title attribute
page.getByTestId('checkout-summary') // 7. Test ID (last semantic)
page.locator('.legacy-widget') // 8. CSS (last resort)
```
## How to Use This Plugin
### Generating Tests
When generating tests, always:
1. Use the `Explore` subagent to scan the project structure first
2. Check `playwright.config.ts` for `testDir`, `baseURL`, and project settings
3. Load relevant templates from `templates/` directory
4. Match the project's language (check for `tsconfig.json` → TypeScript, else JavaScript)
5. Place tests in the configured `testDir` (default: `tests/` or `e2e/`)
6. Include a descriptive test name that explains the behavior being verified
### Reviewing Tests
When reviewing, check against:
1. All 10 golden rules above
2. The anti-patterns in `skills/review/anti-patterns.md`
3. Missing edge cases (empty state, error state, loading state)
4. Proper use of fixtures for shared setup
### Fixing Flaky Tests
When fixing flaky tests:
1. Categorize first: timing, isolation, environment, or infrastructure
2. Use `npx playwright test <file> --repeat-each=10` to reproduce
3. Use `--trace=on` for every attempt
4. Apply the targeted fix from `skills/fix/flaky-taxonomy.md`
### Using Built-in Commands
Leverage Claude Code's built-in capabilities:
- **Large migrations**: Use `/batch` for parallel file-by-file conversion
- **Post-generation cleanup**: Use `/simplify` after generating a test suite
- **Debugging sessions**: Use `/debug` alongside `/pw:fix` for trace analysis
- **Code review**: Use `/review` for general code quality, `/pw:review` for Playwright-specific
### Integrations
- **TestRail**: Configured via `TESTRAIL_URL`, `TESTRAIL_USER`, `TESTRAIL_API_KEY` env vars
- **BrowserStack**: Configured via `BROWSERSTACK_USERNAME`, `BROWSERSTACK_ACCESS_KEY` env vars
- Both are optional. The plugin works fully without them.
## File Conventions
- Test files: `*.spec.ts` or `*.spec.js`
- Page objects: `*.page.ts` in a `pages/` directory
- Fixtures: `fixtures.ts` or `fixtures/` directory
- Test data: `test-data/` directory with JSON/factory files
FILE:README.md
# Playwright Pro
> Production-grade Playwright testing toolkit for AI coding agents.
Generate tests, fix flaky failures, migrate from Cypress/Selenium, sync with TestRail, run on BrowserStack — all from your AI agent.
## Install
```bash
# Claude Code plugin
claude plugin install pw@claude-skills
# Or load directly
claude --plugin-dir ./engineering-team/playwright-pro
```
## Commands
| Command | What it does |
|---|---|
| `/pw:init` | Set up Playwright in your project — detects framework, generates config, CI, first test |
| `/pw:generate <spec>` | Generate tests from a user story, URL, or component name |
| `/pw:review` | Review existing tests for anti-patterns and coverage gaps |
| `/pw:fix <test>` | Diagnose and fix a failing or flaky test |
| `/pw:migrate` | Migrate from Cypress or Selenium to Playwright |
| `/pw:coverage` | Analyze what's tested vs. what's missing |
| `/pw:testrail` | Sync with TestRail — read cases, push results, create runs |
| `/pw:browserstack` | Run tests on BrowserStack, pull cross-browser reports |
| `/pw:report` | Generate a test report in your preferred format |
## Quick Start
```bash
# In Claude Code:
/pw:init # Set up Playwright
/pw:generate "user can log in" # Generate your first test
# Tests are auto-validated by hooks — no extra steps
```
## What's Inside
### 9 Skills
Slash commands that turn natural language into production-ready Playwright tests. Each skill leverages Claude Code's built-in capabilities (`/batch` for parallel work, `Explore` for codebase analysis, `/debug` for trace inspection).
### 3 Specialized Agents
- **test-architect** — Plans test strategy for complex applications
- **test-debugger** — Diagnoses flaky tests using a systematic taxonomy
- **migration-planner** — Creates file-by-file migration plans from Cypress/Selenium
### 55 Test Templates
Ready-to-use, parametrizable templates covering:
| Category | Count | Examples |
|---|---|---|
| Authentication | 8 | Login, logout, SSO, MFA, password reset, RBAC |
| CRUD | 6 | Create, read, update, delete, bulk ops |
| Checkout | 6 | Cart, payment, coupon, order history |
| Search | 5 | Basic search, filters, sorting, pagination |
| Forms | 6 | Multi-step, validation, file upload |
| Dashboard | 5 | Data loading, charts, export |
| Settings | 4 | Profile, password, notifications |
| Onboarding | 4 | Registration, email verify, welcome tour |
| Notifications | 3 | In-app, toast, notification center |
| API | 5 | REST CRUD, GraphQL, error handling |
| Accessibility | 3 | Keyboard nav, screen reader, contrast |
### 2 MCP Integrations
- **TestRail** — Read test cases, create runs, push pass/fail results
- **BrowserStack** — Trigger cross-browser runs, pull session reports with video/screenshots
### Smart Hooks
- Auto-validates test quality when you write `*.spec.ts` files
- Auto-detects Playwright projects on session start
- Zero configuration required
## Integrations Setup
### TestRail (Optional)
Set environment variables:
```bash
export TESTRAIL_URL="https://your-instance.testrail.io"
export TESTRAIL_USER="[email protected]"
export TESTRAIL_API_KEY="your-api-key"
```
Then use `/pw:testrail` to sync test cases and push results.
### BrowserStack (Optional)
```bash
export BROWSERSTACK_USERNAME="your-username"
export BROWSERSTACK_ACCESS_KEY="your-access-key"
```
Then use `/pw:browserstack` to run tests across browsers.
## Works With
| Agent | How |
|---|---|
| **Claude Code** | Full plugin — slash commands, MCP tools, hooks, agents |
| **Codex CLI** | Copy `CLAUDE.md` to your project root as `AGENTS.md` |
| **OpenClaw** | Use as a skill with `SKILL.md` entry point |
## Built-in Command Integration
Playwright Pro doesn't reinvent what your AI agent already does. It orchestrates built-in capabilities:
- `/pw:generate` uses Claude's `Explore` subagent to understand your codebase before generating tests
- `/pw:migrate` uses `/batch` for parallel file-by-file conversion on large test suites
- `/pw:fix` uses `/debug` for trace analysis alongside Playwright-specific diagnostics
- `/pw:review` extends `/review` with Playwright anti-pattern detection
## Reference
Based on battle-tested patterns from production test suites. Includes curated guidance on:
- Locator strategies and priority hierarchy
- Assertion patterns and auto-retry behavior
- Fixture architecture and composition
- Common pitfalls (top 20, ranked by frequency)
- Flaky test diagnosis taxonomy
## License
MIT
FILE:agents/migration-planner.md
---
name: migration-planner
description: >-
Analyzes Cypress or Selenium test suites and creates a file-by-file
migration plan. Invoked by /pw:migrate before conversion starts.
allowed-tools:
- Read
- Grep
- Glob
- LS
---
# Migration Planner Agent
You are a test migration specialist. Your job is to analyze an existing Cypress or Selenium test suite and create a detailed, ordered migration plan.
## Planning Protocol
### Step 1: Detect Source Framework
Scan the project:
**Cypress indicators:**
- `cypress/` directory
- `cypress.config.ts` or `cypress.config.js`
- `@cypress` packages in `package.json`
- `.cy.ts` or `.cy.js` test files
**Selenium indicators:**
- `selenium-webdriver` in dependencies
- `webdriver` or `wdio` in dependencies
- Test files importing `selenium-webdriver`
- `chromedriver` or `geckodriver` in dependencies
- Python files importing `selenium`
### Step 2: Inventory All Test Files
List every test file with:
- File path
- Number of tests (count `it()`, `test()`, or test methods)
- Dependencies (custom commands, page objects, fixtures)
- Complexity (simple/medium/complex based on lines and patterns)
```
## Test Inventory
| # | File | Tests | Dependencies | Complexity |
|---|---|---|---|---|
| 1 | cypress/e2e/login.cy.ts | 5 | login command | Simple |
| 2 | cypress/e2e/checkout.cy.ts | 12 | api helpers, fixtures | Complex |
| 3 | cypress/e2e/search.cy.ts | 8 | none | Medium |
```
### Step 3: Map Dependencies
Identify shared resources that need migration:
**Custom commands** (`cypress/support/commands.ts`):
- List each command and what it does
- Map to Playwright equivalent (fixture, helper function, or page object)
**Fixtures** (`cypress/fixtures/`):
- List data files
- Plan: copy to `test-data/` with any format adjustments
**Plugins** (`cypress/plugins/`):
- List plugin functionality
- Map to Playwright config options or fixtures
**Page Objects** (if used):
- List page object files
- Plan: convert API calls (minimal structural change)
**Support files** (`cypress/support/`):
- List setup/teardown logic
- Map to `playwright.config.ts` or `fixtures/`
### Step 4: Determine Migration Order
Order files by dependency graph:
1. **Shared resources first**: custom commands → fixtures, page objects → helpers
2. **Simple tests next**: files with no dependencies, few tests
3. **Complex tests last**: files with many dependencies, custom commands
```
## Migration Order
### Phase 1: Foundation (do first)
1. Convert custom commands → fixtures.ts
2. Copy fixtures → test-data/
3. Convert page objects (API changes only)
### Phase 2: Simple Tests (quick wins)
4. login.cy.ts → auth/login.spec.ts (5 tests, ~15 min)
5. about.cy.ts → static/about.spec.ts (2 tests, ~5 min)
### Phase 3: Complex Tests
6. checkout.cy.ts → checkout/checkout.spec.ts (12 tests, ~45 min)
7. search.cy.ts → search/search.spec.ts (8 tests, ~30 min)
```
### Step 5: Estimate Effort
| Complexity | Time per test | Notes |
|---|---|---|
| Simple | 2-3 min | Direct API mapping |
| Medium | 5-10 min | Needs locator upgrade |
| Complex | 10-20 min | Custom commands, plugins, complex flows |
### Step 6: Identify Risks
Flag tests that may need manual intervention:
- Tests using Cypress-only features (`cy.origin()`, `cy.session()`)
- Tests with complex `cy.intercept()` patterns
- Tests relying on Cypress retry-ability semantics
- Tests using Cypress plugins with no Playwright equivalent
### Step 7: Return Plan
Return the complete migration plan to `/pw:migrate` for execution.
FILE:agents/test-architect.md
---
name: test-architect
description: >-
Plans test strategy for complex applications. Invoked by /pw:generate and
/pw:coverage when the app has multiple routes, complex state, or requires
a structured test plan before writing tests.
allowed-tools:
- Read
- Grep
- Glob
- LS
---
# Test Architect Agent
You are a test architecture specialist. Your job is to analyze an application's structure and create a comprehensive test plan before any tests are written.
## Your Responsibilities
1. **Map the application surface**: routes, components, API endpoints, user flows
2. **Identify critical paths**: the flows that, if broken, cause revenue loss or user churn
3. **Design test structure**: folder organization, fixture strategy, data management
4. **Prioritize**: which tests deliver the most confidence per effort
5. **Select patterns**: which template or approach fits each test scenario
## How You Work
You are a read-only agent. You analyze and plan — you do not write test files.
### Step 1: Scan the Codebase
- Read route definitions (Next.js `app/`, React Router, Vue Router, Angular routes)
- Read `package.json` for framework and dependencies
- Check for existing tests and their patterns
- Identify state management (Redux, Zustand, Pinia, etc.)
- Check for API layer (REST, GraphQL, tRPC)
### Step 2: Catalog Testable Surfaces
Create a structured inventory:
```
## Application Surface
### Pages (by priority)
1. /login — Auth entry point [CRITICAL]
2. /dashboard — Main user view [CRITICAL]
3. /settings — User preferences [HIGH]
4. /admin — Admin panel [HIGH]
5. /about — Static page [LOW]
### Interactive Components
1. SearchBar — complex state, debounced API calls
2. DataTable — sorting, filtering, pagination
3. FileUploader — drag-drop, progress, error handling
### API Endpoints
1. POST /api/auth/login — authentication
2. GET /api/users — user list with pagination
3. PUT /api/users/:id — user update
### User Flows (multi-page)
1. Registration → Email Verify → Onboarding → Dashboard
2. Search → Filter → Select → Add to Cart → Checkout → Confirm
```
### Step 3: Design Test Plan
```
## Test Plan
### Folder Structure
e2e/
├── auth/ # Authentication tests
├── dashboard/ # Dashboard tests
├── checkout/ # Checkout flow tests
├── fixtures/ # Shared fixtures
├── pages/ # Page object models
└── test-data/ # Test data files
### Fixture Strategy
- Auth fixture: shared `storageState` for logged-in tests
- API fixture: request context for data seeding
- Data fixture: factory functions for test entities
### Test Distribution
| Area | Tests | Template | Effort |
|---|---|---|---|
| Auth | 8 | auth/* | 1h |
| Dashboard | 6 | dashboard/* | 1h |
| Checkout | 10 | checkout/* | 2h |
| Search | 5 | search/* | 45m |
| Settings | 4 | settings/* | 30m |
| API | 5 | api/* | 45m |
### Priority Order
1. Auth (blocks everything else)
2. Core user flow (the main thing users do)
3. Payment/checkout (revenue-critical)
4. Everything else
```
### Step 4: Return Plan
Return the complete plan to the calling skill. Do not write files.
FILE:agents/test-debugger.md
---
name: test-debugger
description: >-
Diagnoses flaky or failing Playwright tests using systematic taxonomy.
Invoked by /pw:fix when a test needs deep analysis including running
tests, reading traces, and identifying root causes.
allowed-tools:
- Read
- Grep
- Glob
- LS
- Bash
---
# Test Debugger Agent
You are a Playwright test debugging specialist. Your job is to systematically diagnose why a test fails or behaves flakily, identify the root cause category, and return a specific fix.
## Debugging Protocol
### Step 1: Read the Test
Read the test file and understand:
- What behavior it's testing
- Which pages/URLs it visits
- Which locators it uses
- Which assertions it makes
- Any setup/teardown (fixtures, beforeEach)
### Step 2: Run the Test
Run it multiple ways to classify the failure:
```bash
# Single run — get the error
npx playwright test <file> --grep "<test name>" --reporter=list 2>&1
# Burn-in — expose timing issues
npx playwright test <file> --grep "<test name>" --repeat-each=10 --reporter=list 2>&1
# Isolation check — expose state leaks
npx playwright test <file> --grep "<test name>" --workers=1 --reporter=list 2>&1
# Full suite — expose interaction
npx playwright test --reporter=list 2>&1
```
### Step 3: Capture Trace
```bash
npx playwright test <file> --grep "<test name>" --trace=on --retries=0 2>&1
```
Read the trace output for:
- Network requests that failed or were slow
- Elements that weren't visible when expected
- Navigation timing issues
- Console errors
### Step 4: Classify
| Category | Evidence |
|---|---|
| **Timing/Async** | Fails on `--repeat-each=10`; error mentions timeout or element not found intermittently |
| **Test Isolation** | Passes alone (`--workers=1 --grep`), fails in full suite |
| **Environment** | Passes locally, fails in CI (check viewport, fonts, timezone) |
| **Infrastructure** | Random crash errors, OOM, browser process killed |
### Step 5: Identify Specific Cause
Common root causes per category:
**Timing:**
- Missing `await` on a Playwright call
- `waitForTimeout()` that's too short
- Clicking before element is actionable
- Asserting before data loads
- Animation interference
**Isolation:**
- Global variable shared between tests
- Database not cleaned between tests
- localStorage/cookies leaking
- Test creates data with non-unique identifier
**Environment:**
- Different viewport size in CI
- Font rendering differences affect screenshots
- Timezone affects date assertions
- Network latency in CI is higher
**Infrastructure:**
- Browser runs out of memory with too many workers
- File system race condition
- DNS resolution failure
### Step 6: Return Diagnosis
Return to the calling skill:
```
## Diagnosis
**Category:** Timing/Async
**Root Cause:** Missing await on line 23 — `page.goto('/dashboard')` runs without
waiting, so the assertion on line 24 runs before navigation completes.
**Evidence:** Fails 3/10 times on `--repeat-each=10`. Trace shows assertion firing
before navigation response received.
## Fix
Line 23: Add `await` before `page.goto('/dashboard')`
## Verification
After fix: 10/10 passes on `--repeat-each=10`
```
FILE:hooks/detect-playwright.sh
#!/usr/bin/env bash
# Session start hook: detects if the project uses Playwright.
# Outputs context hint for Claude if playwright.config exists.
set -euo pipefail
# Check for Playwright config in current directory or common locations
PW_CONFIG=""
for config in playwright.config.ts playwright.config.js playwright.config.mjs; do
if [[ -f "$config" ]]; then
PW_CONFIG="$config"
break
fi
done
if [[ -z "$PW_CONFIG" ]]; then
exit 0
fi
# Count existing test files
TEST_COUNT=$(find . -name "*.spec.ts" -o -name "*.spec.js" -o -name "*.test.ts" -o -name "*.test.js" 2>/dev/null | grep -v node_modules | wc -l | tr -d ' ')
echo "🎭 Playwright detected ($PW_CONFIG) — $TEST_COUNT test files found. Use /pw: commands for testing workflows."
FILE:hooks/hooks.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash CLAUDE_PLUGIN_ROOT/hooks/validate-test.sh"
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash CLAUDE_PLUGIN_ROOT/hooks/detect-playwright.sh"
}
]
}
]
}
}
FILE:hooks/validate-test.sh
#!/usr/bin/env bash
# Post-write hook: validates Playwright test files for common anti-patterns.
# Runs silently — only outputs warnings if issues found.
# Input: JSON on stdin with tool_input.file_path
set -euo pipefail
# Read the file path from stdin JSON
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
print(data.get('tool_input', {}).get('file_path', ''))
except:
print('')
" 2>/dev/null || echo "")
# Only check .spec.ts and .spec.js files
if [[ ! "$FILE_PATH" =~ \.(spec|test)\.(ts|js|mjs)$ ]]; then
exit 0
fi
# Check if file exists
if [[ ! -f "$FILE_PATH" ]]; then
exit 0
fi
WARNINGS=""
# Check for waitForTimeout
if grep -n 'waitForTimeout' "$FILE_PATH" >/dev/null 2>&1; then
LINES=$(grep -n 'waitForTimeout' "$FILE_PATH" | head -3)
WARNINGS="WARNINGS\n⚠️ waitForTimeout() found — use web-first assertions instead:\nLINES\n"
fi
# Check for non-web-first assertions
if grep -n 'expect(await ' "$FILE_PATH" >/dev/null 2>&1; then
LINES=$(grep -n 'expect(await ' "$FILE_PATH" | head -3)
WARNINGS="WARNINGS\n⚠️ Non-web-first assertion — use expect(locator) instead:\nLINES\n"
fi
# Check for hardcoded localhost URLs
if grep -n "http://localhost\|https://localhost\|http://127.0.0.1" "$FILE_PATH" >/dev/null 2>&1; then
LINES=$(grep -n "http://localhost\|https://localhost\|http://127.0.0.1" "$FILE_PATH" | head -3)
WARNINGS="WARNINGS\n⚠️ Hardcoded URL — use baseURL from config:\nLINES\n"
fi
# Check for page.$() usage
if grep -n 'page\.\$(' "$FILE_PATH" >/dev/null 2>&1; then
LINES=$(grep -n 'page\.\$(' "$FILE_PATH" | head -3)
WARNINGS="WARNINGS\n⚠️ page.\$() is deprecated — use page.locator() or getByRole():\nLINES\n"
fi
# Output warnings if any found
if [[ -n "$WARNINGS" ]]; then
echo -e "\n🎭 Playwright Pro — Test ValidationWARNINGS"
fi
FILE:integrations/browserstack-mcp/package.json
{
"name": "@pw/browserstack-mcp",
"version": "1.0.0",
"description": "MCP server for BrowserStack integration with Playwright Pro",
"type": "module",
"main": "src/index.ts",
"scripts": {
"start": "tsx src/index.ts",
"build": "tsc"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
},
"devDependencies": {
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}
FILE:integrations/browserstack-mcp/src/client.ts
import type {
BrowserStackConfig,
BrowserStackPlan,
BrowserStackBrowser,
BrowserStackBuild,
BrowserStackSession,
BrowserStackSessionUpdate,
} from './types.js';
export class BrowserStackClient {
private readonly baseUrl = 'https://api.browserstack.com';
private readonly headers: Record<string, string>;
constructor(config: BrowserStackConfig) {
const auth = Buffer.from(`config.username:config.accessKey`).toString('base64');
this.headers = {
Authorization: `Basic auth`,
'Content-Type': 'application/json',
};
}
private async request<T>(
method: string,
endpoint: string,
body?: unknown,
): Promise<T> {
const url = `this.baseUrlendpoint`;
const options: RequestInit = {
method,
headers: this.headers,
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`BrowserStack API error response.status: errorText`,
);
}
return response.json() as Promise<T>;
}
async getPlan(): Promise<BrowserStackPlan> {
return this.request<BrowserStackPlan>('GET', '/automate/plan.json');
}
async getBrowsers(): Promise<BrowserStackBrowser[]> {
return this.request<BrowserStackBrowser[]>('GET', '/automate/browsers.json');
}
async getBuilds(limit?: number, status?: string): Promise<BrowserStackBuild[]> {
let endpoint = '/automate/builds.json';
const params: string[] = [];
if (limit) params.push(`limit=limit`);
if (status) params.push(`status=status`);
if (params.length > 0) endpoint += `?params.join('&')`;
return this.request<BrowserStackBuild[]>('GET', endpoint);
}
async getSessions(buildId: string, limit?: number): Promise<BrowserStackSession[]> {
let endpoint = `/automate/builds/buildId/sessions.json`;
if (limit) endpoint += `?limit=limit`;
return this.request<BrowserStackSession[]>('GET', endpoint);
}
async getSession(sessionId: string): Promise<BrowserStackSession> {
return this.request<BrowserStackSession>(
'GET',
`/automate/sessions/sessionId.json`,
);
}
async updateSession(
sessionId: string,
update: BrowserStackSessionUpdate,
): Promise<BrowserStackSession> {
return this.request<BrowserStackSession>(
'PUT',
`/automate/sessions/sessionId.json`,
update,
);
}
async getSessionLogs(sessionId: string): Promise<string> {
const url = `this.baseUrl/automate/sessions/sessionId/logs`;
const response = await fetch(url, { headers: this.headers });
if (!response.ok) {
throw new Error(`BrowserStack logs error response.status`);
}
return response.text();
}
}
FILE:integrations/browserstack-mcp/src/index.ts
#!/usr/bin/env npx tsx
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { BrowserStackClient } from './client.js';
import type { BrowserStackSessionUpdate } from './types.js';
const config = {
username: process.env.BROWSERSTACK_USERNAME ?? '',
accessKey: process.env.BROWSERSTACK_ACCESS_KEY ?? '',
};
if (!config.username || !config.accessKey) {
console.error(
'Missing BrowserStack configuration. Set BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY.',
);
process.exit(1);
}
const client = new BrowserStackClient(config);
const server = new Server(
{ name: 'pw-browserstack', version: '1.0.0' },
{ capabilities: { tools: {} } },
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'browserstack_get_plan',
description: 'Get BrowserStack Automate plan details including parallel session limits',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'browserstack_get_browsers',
description: 'List all available browser and OS combinations for Playwright testing',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'browserstack_get_builds',
description: 'List recent test builds with status',
inputSchema: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Max builds to return (default 10)' },
status: {
type: 'string',
enum: ['running', 'done', 'failed', 'timeout'],
description: 'Filter by status',
},
},
},
},
{
name: 'browserstack_get_sessions',
description: 'List test sessions within a build',
inputSchema: {
type: 'object',
properties: {
build_id: { type: 'string', description: 'Build hashed ID' },
limit: { type: 'number', description: 'Max sessions to return' },
},
required: ['build_id'],
},
},
{
name: 'browserstack_get_session',
description: 'Get detailed session info including video URL, logs, and screenshots',
inputSchema: {
type: 'object',
properties: {
session_id: { type: 'string', description: 'Session hashed ID' },
},
required: ['session_id'],
},
},
{
name: 'browserstack_update_session',
description: 'Update session status (mark as passed/failed) and name',
inputSchema: {
type: 'object',
properties: {
session_id: { type: 'string', description: 'Session hashed ID' },
status: {
type: 'string',
enum: ['passed', 'failed'],
description: 'Test result status',
},
name: { type: 'string', description: 'Updated session name' },
reason: { type: 'string', description: 'Reason for failure' },
},
required: ['session_id'],
},
},
{
name: 'browserstack_get_logs',
description: 'Get text logs for a specific test session',
inputSchema: {
type: 'object',
properties: {
session_id: { type: 'string', description: 'Session hashed ID' },
},
required: ['session_id'],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'browserstack_get_plan': {
const plan = await client.getPlan();
return { content: [{ type: 'text', text: JSON.stringify(plan, null, 2) }] };
}
case 'browserstack_get_browsers': {
const browsers = await client.getBrowsers();
const playwrightBrowsers = browsers.filter(
(b) =>
['chrome', 'firefox', 'playwright-chromium', 'playwright-firefox', 'playwright-webkit'].includes(
b.browser?.toLowerCase() ?? '',
) || b.browser?.toLowerCase().includes('playwright'),
);
const summary = playwrightBrowsers.length > 0 ? playwrightBrowsers : browsers.slice(0, 50);
return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] };
}
case 'browserstack_get_builds': {
const builds = await client.getBuilds(
(args?.limit as number) ?? 10,
args?.status as string | undefined,
);
return { content: [{ type: 'text', text: JSON.stringify(builds, null, 2) }] };
}
case 'browserstack_get_sessions': {
const sessions = await client.getSessions(
args!.build_id as string,
args?.limit as number | undefined,
);
return { content: [{ type: 'text', text: JSON.stringify(sessions, null, 2) }] };
}
case 'browserstack_get_session': {
const session = await client.getSession(args!.session_id as string);
return { content: [{ type: 'text', text: JSON.stringify(session, null, 2) }] };
}
case 'browserstack_update_session': {
const update: BrowserStackSessionUpdate = {};
if (args?.status) update.status = args.status as 'passed' | 'failed';
if (args?.name) update.name = args.name as string;
if (args?.reason) update.reason = args.reason as string;
const updated = await client.updateSession(args!.session_id as string, update);
return { content: [{ type: 'text', text: JSON.stringify(updated, null, 2) }] };
}
case 'browserstack_get_logs': {
const logs = await client.getSessionLogs(args!.session_id as string);
return { content: [{ type: 'text', text: logs }] };
}
default:
return { content: [{ type: 'text', text: `Unknown tool: name` }], isError: true };
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { content: [{ type: 'text', text: `Error: message` }], isError: true };
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
FILE:integrations/browserstack-mcp/src/types.ts
export interface BrowserStackConfig {
username: string;
accessKey: string;
}
export interface BrowserStackPlan {
automate_plan: string;
parallel_sessions_running: number;
team_parallel_sessions_max_allowed: number;
parallel_sessions_max_allowed: number;
queued_sessions: number;
queued_sessions_max_allowed: number;
}
export interface BrowserStackBrowser {
os: string;
os_version: string;
browser: string;
browser_version: string;
device: string | null;
real_mobile: boolean | null;
}
export interface BrowserStackBuild {
automation_build: {
name: string;
hashed_id: string;
duration: number;
status: string;
build_tag: string | null;
};
}
export interface BrowserStackSession {
automation_session: {
name: string;
duration: number;
os: string;
os_version: string;
browser_version: string;
browser: string;
device: string | null;
status: string;
hashed_id: string;
reason: string;
build_name: string;
project_name: string;
logs: string;
browser_url: string;
public_url: string;
video_url: string;
browser_console_logs_url: string;
har_logs_url: string;
};
}
export interface BrowserStackSessionUpdate {
name?: string;
status?: 'passed' | 'failed';
reason?: string;
}
FILE:integrations/browserstack-mcp/tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
FILE:integrations/testrail-mcp/package.json
{
"name": "@pw/testrail-mcp",
"version": "1.0.0",
"description": "MCP server for TestRail integration with Playwright Pro",
"type": "module",
"main": "src/index.ts",
"scripts": {
"start": "tsx src/index.ts",
"build": "tsc"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
},
"devDependencies": {
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}
FILE:integrations/testrail-mcp/src/client.ts
import type {
TestRailConfig,
TestRailProject,
TestRailSuite,
TestRailCase,
TestRailCasePayload,
TestRailRun,
TestRailRunPayload,
TestRailResult,
TestRailResultPayload,
} from './types.js';
export class TestRailClient {
private readonly baseUrl: string;
private readonly headers: Record<string, string>;
constructor(config: TestRailConfig) {
this.baseUrl = config.url.replace(/\/+$/, '');
const auth = Buffer.from(`config.user:config.apiKey`).toString('base64');
this.headers = {
Authorization: `Basic auth`,
'Content-Type': 'application/json',
};
}
private async request<T>(
method: string,
endpoint: string,
body?: unknown,
): Promise<T> {
const url = `this.baseUrl/index.php?/api/v2/endpoint`;
const options: RequestInit = {
method,
headers: this.headers,
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`TestRail API error response.status: errorText`,
);
}
return response.json() as Promise<T>;
}
async getProjects(): Promise<TestRailProject[]> {
const result = await this.request<{ projects: TestRailProject[] }>(
'GET',
'get_projects',
);
return result.projects ?? result as unknown as TestRailProject[];
}
async getSuites(projectId: number): Promise<TestRailSuite[]> {
return this.request<TestRailSuite[]>('GET', `get_suites/projectId`);
}
async getCases(
projectId: number,
suiteId?: number,
sectionId?: number,
limit?: number,
offset?: number,
filter?: string,
): Promise<TestRailCase[]> {
let endpoint = `get_cases/projectId`;
const params: string[] = [];
if (suiteId) params.push(`suite_id=suiteId`);
if (sectionId) params.push(`section_id=sectionId`);
if (limit) params.push(`limit=limit`);
if (offset) params.push(`offset=offset`);
if (filter) params.push(`filter=encodeURIComponent(filter)`);
if (params.length > 0) endpoint += `¶ms.join('&')`;
const result = await this.request<{ cases: TestRailCase[] }>(
'GET',
endpoint,
);
return result.cases ?? result as unknown as TestRailCase[];
}
async addCase(
sectionId: number,
payload: TestRailCasePayload,
): Promise<TestRailCase> {
return this.request<TestRailCase>(
'POST',
`add_case/sectionId`,
payload,
);
}
async updateCase(
caseId: number,
payload: Partial<TestRailCasePayload>,
): Promise<TestRailCase> {
return this.request<TestRailCase>(
'POST',
`update_case/caseId`,
payload,
);
}
async addRun(
projectId: number,
payload: TestRailRunPayload,
): Promise<TestRailRun> {
return this.request<TestRailRun>(
'POST',
`add_run/projectId`,
payload,
);
}
async addResultForCase(
runId: number,
caseId: number,
payload: TestRailResultPayload,
): Promise<TestRailResult> {
return this.request<TestRailResult>(
'POST',
`add_result_for_case/runId/caseId`,
payload,
);
}
async getResultsForCase(
runId: number,
caseId: number,
limit?: number,
): Promise<TestRailResult[]> {
let endpoint = `get_results_for_case/runId/caseId`;
if (limit) endpoint += `&limit=limit`;
const result = await this.request<{ results: TestRailResult[] }>(
'GET',
endpoint,
);
return result.results ?? result as unknown as TestRailResult[];
}
}
FILE:integrations/testrail-mcp/src/index.ts
#!/usr/bin/env npx tsx
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { TestRailClient } from './client.js';
import type { TestRailCasePayload, TestRailRunPayload, TestRailResultPayload } from './types.js';
const config = {
url: process.env.TESTRAIL_URL ?? '',
user: process.env.TESTRAIL_USER ?? '',
apiKey: process.env.TESTRAIL_API_KEY ?? '',
};
if (!config.url || !config.user || !config.apiKey) {
console.error(
'Missing TestRail configuration. Set TESTRAIL_URL, TESTRAIL_USER, and TESTRAIL_API_KEY.',
);
process.exit(1);
}
const client = new TestRailClient(config);
const server = new Server(
{ name: 'pw-testrail', version: '1.0.0' },
{ capabilities: { tools: {} } },
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'testrail_get_projects',
description: 'List all TestRail projects',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'testrail_get_suites',
description: 'List test suites in a project',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'number', description: 'Project ID' },
},
required: ['project_id'],
},
},
{
name: 'testrail_get_cases',
description: 'Get test cases from a project. Supports filtering by suite, section, and search text.',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'number', description: 'Project ID' },
suite_id: { type: 'number', description: 'Suite ID (optional)' },
section_id: { type: 'number', description: 'Section ID (optional)' },
limit: { type: 'number', description: 'Max results (default 250)' },
offset: { type: 'number', description: 'Offset for pagination' },
filter: { type: 'string', description: 'Search text filter' },
},
required: ['project_id'],
},
},
{
name: 'testrail_add_case',
description: 'Create a new test case in a section',
inputSchema: {
type: 'object',
properties: {
section_id: { type: 'number', description: 'Section ID to add the case to' },
title: { type: 'string', description: 'Test case title' },
template_id: { type: 'number', description: 'Template ID (2 = Test Case Steps)' },
priority_id: { type: 'number', description: 'Priority (1=Low, 2=Medium, 3=High, 4=Critical)' },
custom_preconds: { type: 'string', description: 'Preconditions text' },
custom_steps_separated: {
type: 'array',
items: {
type: 'object',
properties: {
content: { type: 'string', description: 'Step action' },
expected: { type: 'string', description: 'Expected result' },
},
},
description: 'Test steps with expected results',
},
},
required: ['section_id', 'title'],
},
},
{
name: 'testrail_update_case',
description: 'Update an existing test case',
inputSchema: {
type: 'object',
properties: {
case_id: { type: 'number', description: 'Case ID to update' },
title: { type: 'string', description: 'Updated title' },
custom_preconds: { type: 'string', description: 'Updated preconditions' },
custom_steps_separated: {
type: 'array',
items: {
type: 'object',
properties: {
content: { type: 'string' },
expected: { type: 'string' },
},
},
description: 'Updated test steps',
},
},
required: ['case_id'],
},
},
{
name: 'testrail_add_run',
description: 'Create a new test run in a project',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'number', description: 'Project ID' },
name: { type: 'string', description: 'Run name' },
description: { type: 'string', description: 'Run description' },
suite_id: { type: 'number', description: 'Suite ID' },
include_all: { type: 'boolean', description: 'Include all cases (default true)' },
case_ids: {
type: 'array',
items: { type: 'number' },
description: 'Specific case IDs to include (if include_all is false)',
},
},
required: ['project_id', 'name'],
},
},
{
name: 'testrail_add_result',
description: 'Add a test result for a specific case in a run',
inputSchema: {
type: 'object',
properties: {
run_id: { type: 'number', description: 'Run ID' },
case_id: { type: 'number', description: 'Case ID' },
status_id: {
type: 'number',
description: 'Status: 1=Passed, 2=Blocked, 3=Untested, 4=Retest, 5=Failed',
},
comment: { type: 'string', description: 'Result comment or error message' },
elapsed: { type: 'string', description: 'Time spent (e.g., "30s", "1m 45s")' },
defects: { type: 'string', description: 'Defect IDs (comma-separated)' },
},
required: ['run_id', 'case_id', 'status_id'],
},
},
{
name: 'testrail_get_results',
description: 'Get historical results for a test case in a run',
inputSchema: {
type: 'object',
properties: {
run_id: { type: 'number', description: 'Run ID' },
case_id: { type: 'number', description: 'Case ID' },
limit: { type: 'number', description: 'Max results to return' },
},
required: ['run_id', 'case_id'],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'testrail_get_projects': {
const projects = await client.getProjects();
return { content: [{ type: 'text', text: JSON.stringify(projects, null, 2) }] };
}
case 'testrail_get_suites': {
const suites = await client.getSuites(args!.project_id as number);
return { content: [{ type: 'text', text: JSON.stringify(suites, null, 2) }] };
}
case 'testrail_get_cases': {
const cases = await client.getCases(
args!.project_id as number,
args?.suite_id as number | undefined,
args?.section_id as number | undefined,
args?.limit as number | undefined,
args?.offset as number | undefined,
args?.filter as string | undefined,
);
return { content: [{ type: 'text', text: JSON.stringify(cases, null, 2) }] };
}
case 'testrail_add_case': {
const payload: TestRailCasePayload = {
title: args!.title as string,
template_id: args?.template_id as number | undefined,
priority_id: args?.priority_id as number | undefined,
custom_preconds: args?.custom_preconds as string | undefined,
custom_steps_separated: args?.custom_steps_separated as TestRailCasePayload['custom_steps_separated'],
};
const newCase = await client.addCase(args!.section_id as number, payload);
return { content: [{ type: 'text', text: JSON.stringify(newCase, null, 2) }] };
}
case 'testrail_update_case': {
const updatePayload: Partial<TestRailCasePayload> = {};
if (args?.title) updatePayload.title = args.title as string;
if (args?.custom_preconds) updatePayload.custom_preconds = args.custom_preconds as string;
if (args?.custom_steps_separated) {
updatePayload.custom_steps_separated = args.custom_steps_separated as TestRailCasePayload['custom_steps_separated'];
}
const updated = await client.updateCase(args!.case_id as number, updatePayload);
return { content: [{ type: 'text', text: JSON.stringify(updated, null, 2) }] };
}
case 'testrail_add_run': {
const runPayload: TestRailRunPayload = {
name: args!.name as string,
description: args?.description as string | undefined,
suite_id: args?.suite_id as number | undefined,
include_all: (args?.include_all as boolean) ?? true,
case_ids: args?.case_ids as number[] | undefined,
};
const run = await client.addRun(args!.project_id as number, runPayload);
return { content: [{ type: 'text', text: JSON.stringify(run, null, 2) }] };
}
case 'testrail_add_result': {
const resultPayload: TestRailResultPayload = {
status_id: args!.status_id as number,
comment: args?.comment as string | undefined,
elapsed: args?.elapsed as string | undefined,
defects: args?.defects as string | undefined,
};
const result = await client.addResultForCase(
args!.run_id as number,
args!.case_id as number,
resultPayload,
);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
case 'testrail_get_results': {
const results = await client.getResultsForCase(
args!.run_id as number,
args!.case_id as number,
args?.limit as number | undefined,
);
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
}
default:
return { content: [{ type: 'text', text: `Unknown tool: name` }], isError: true };
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { content: [{ type: 'text', text: `Error: message` }], isError: true };
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
FILE:integrations/testrail-mcp/src/types.ts
export interface TestRailConfig {
url: string;
user: string;
apiKey: string;
}
export interface TestRailProject {
id: number;
name: string;
announcement: string;
is_completed: boolean;
suite_mode: number;
url: string;
}
export interface TestRailSuite {
id: number;
name: string;
description: string | null;
project_id: number;
url: string;
}
export interface TestRailSection {
id: number;
suite_id: number;
name: string;
description: string | null;
parent_id: number | null;
depth: number;
}
export interface TestRailCaseStep {
content: string;
expected: string;
}
export interface TestRailCase {
id: number;
title: string;
section_id: number;
template_id: number;
type_id: number;
priority_id: number;
estimate: string | null;
refs: string | null;
custom_preconds: string | null;
custom_steps_separated: TestRailCaseStep[] | null;
custom_steps: string | null;
custom_expected: string | null;
}
export interface TestRailRun {
id: number;
suite_id: number;
name: string;
description: string | null;
assignedto_id: number | null;
include_all: boolean;
is_completed: boolean;
passed_count: number;
failed_count: number;
untested_count: number;
url: string;
}
export interface TestRailResult {
id: number;
test_id: number;
status_id: number;
comment: string | null;
created_on: number;
elapsed: string | null;
defects: string | null;
}
export interface TestRailResultPayload {
status_id: number;
comment?: string;
elapsed?: string;
defects?: string;
}
export interface TestRailRunPayload {
suite_id?: number;
name: string;
description?: string;
assignedto_id?: number;
include_all?: boolean;
case_ids?: number[];
refs?: string;
}
export interface TestRailCasePayload {
title: string;
template_id?: number;
type_id?: number;
priority_id?: number;
estimate?: string;
refs?: string;
custom_preconds?: string;
custom_steps_separated?: TestRailCaseStep[];
custom_steps?: string;
custom_expected?: string;
}
FILE:integrations/testrail-mcp/tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
FILE:reference/assertions.md
# Assertions Reference
## Web-First Assertions (Always Use These)
Auto-retry until timeout. Safe for dynamic content.
```typescript
// Visibility
await expect(locator).toBeVisible();
await expect(locator).not.toBeVisible();
await expect(locator).toBeHidden();
// Text
await expect(locator).toHaveText('exact text');
await expect(locator).toHaveText(/partial/i);
await expect(locator).toContainText('partial');
// Value (inputs)
await expect(locator).toHaveValue('entered text');
await expect(locator).toHaveValues(['option1', 'option2']);
// Attributes
await expect(locator).toHaveAttribute('href', '/dashboard');
await expect(locator).toHaveClass(/active/);
await expect(locator).toHaveId('main-nav');
// State
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();
await expect(locator).toBeEditable();
await expect(locator).toBeFocused();
await expect(locator).toBeAttached();
// Count
await expect(locator).toHaveCount(5);
await expect(locator).toHaveCount(0); // element doesn't exist
// CSS
await expect(locator).toHaveCSS('color', 'rgb(255, 0, 0)');
// Screenshots
await expect(locator).toHaveScreenshot('button.png');
await expect(page).toHaveScreenshot('full-page.png');
```
## Page Assertions
```typescript
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveURL(/\/dashboard/);
await expect(page).toHaveTitle('Dashboard - App');
await expect(page).toHaveTitle(/Dashboard/);
```
## Anti-Patterns (Never Do This)
```typescript
// BAD — no auto-retry
const text = await locator.textContent();
expect(text).toBe('Hello');
// BAD — snapshot in time, not reactive
const isVisible = await locator.isVisible();
expect(isVisible).toBe(true);
// BAD — evaluating in page context
const value = await page.evaluate(() =>
document.querySelector('input')?.value
);
expect(value).toBe('test');
```
## Custom Timeout
```typescript
// Override timeout for slow operations
await expect(locator).toBeVisible({ timeout: 30_000 });
```
## Soft Assertions
Continue test even if assertion fails (report all failures at end):
```typescript
await expect.soft(locator).toHaveText('Expected');
await expect.soft(page).toHaveURL('/next');
// Test continues even if above fail
```
FILE:reference/common-pitfalls.md
# Common Pitfalls (Top 10)
## 1. waitForTimeout
**Symptom:** Slow, flaky tests.
```typescript
// BAD
await page.waitForTimeout(3000);
// GOOD
await expect(page.getByTestId('result')).toBeVisible();
```
## 2. Non-Web-First Assertions
**Symptom:** Assertions fail on dynamic content.
```typescript
// BAD — checks once, no retry
const text = await page.textContent('.msg');
expect(text).toBe('Done');
// GOOD — retries until timeout
await expect(page.getByText('Done')).toBeVisible();
```
## 3. Missing await
**Symptom:** Random passes/failures, tests seem to skip steps.
```typescript
// BAD
page.goto('/dashboard');
expect(page.getByText('Welcome')).toBeVisible();
// GOOD
await page.goto('/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();
```
## 4. Hardcoded URLs
**Symptom:** Tests break in different environments.
```typescript
// BAD
await page.goto('http://localhost:3000/login');
// GOOD — uses baseURL from config
await page.goto('/login');
```
## 5. CSS Selectors Instead of Roles
**Symptom:** Tests break after CSS refactors.
```typescript
// BAD
await page.click('#submit-btn');
// GOOD
await page.getByRole('button', { name: 'Submit' }).click();
```
## 6. Shared State Between Tests
**Symptom:** Tests pass alone, fail in suite.
```typescript
// BAD — test B depends on test A
let userId: string;
test('create user', async () => { userId = '123'; });
test('edit user', async () => { /* uses userId */ });
// GOOD — each test is independent
test('edit user', async ({ request }) => {
const res = await request.post('/api/users', { data: { name: 'Test' } });
const { id } = await res.json();
// ...
});
```
## 7. Using networkidle
**Symptom:** Tests hang or timeout unpredictably.
```typescript
// BAD — waits for all network activity to stop
await page.goto('/dashboard', { waitUntil: 'networkidle' });
// GOOD — wait for specific content
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
```
## 8. Not Waiting for Navigation
**Symptom:** Assertions run on wrong page.
```typescript
// BAD — click navigates but we don't wait
await page.getByRole('link', { name: 'Settings' }).click();
await expect(page.getByRole('heading')).toHaveText('Settings');
// GOOD — wait for URL change
await page.getByRole('link', { name: 'Settings' }).click();
await expect(page).toHaveURL('/settings');
await expect(page.getByRole('heading')).toHaveText('Settings');
```
## 9. Testing Implementation, Not Behavior
**Symptom:** Tests break on every refactor.
```typescript
// BAD — tests CSS class (implementation detail)
await expect(page.locator('.btn')).toHaveClass('btn-primary active');
// GOOD — tests what the user sees
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled();
```
## 10. No Error Case Tests
**Symptom:** App breaks on errors but all tests pass.
```typescript
// Missing: what happens when the API fails?
test('should handle API error', async ({ page }) => {
await page.route('**/api/data', (route) =>
route.fulfill({ status: 500 })
);
await page.goto('/dashboard');
await expect(page.getByText(/error|try again/i)).toBeVisible();
});
```
FILE:reference/fixtures.md
# Fixtures Reference
## What Are Fixtures
Fixtures provide setup/teardown for each test. They replace `beforeEach`/`afterEach` for shared state and are composable, type-safe, and lazy (only run when used).
## Creating Custom Fixtures
```typescript
// fixtures.ts
import { test as base, expect } from '@playwright/test';
// Define fixture types
type MyFixtures = {
authenticatedPage: Page;
testUser: { email: string; password: string };
apiClient: APIRequestContext;
};
export const test = base.extend<MyFixtures>({
// Simple value fixture
testUser: async ({}, use) => {
await use({
email: `test-Date.now()@example.com`,
password: 'Test123!',
});
},
// Fixture with setup and teardown
authenticatedPage: async ({ page, testUser }, use) => {
// Setup: log in
await page.goto('/login');
await page.getByLabel('Email').fill(testUser.email);
await page.getByLabel('Password').fill(testUser.password);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/dashboard');
// Provide the authenticated page to the test
await use(page);
// Teardown: clean up (optional)
await page.goto('/logout');
},
// API client fixture
apiClient: async ({ playwright }, use) => {
const context = await playwright.request.newContext({
baseURL: 'http://localhost:3000',
extraHTTPHeaders: {
Authorization: `Bearer process.env.API_TOKEN`,
},
});
await use(context);
await context.dispose();
},
});
export { expect };
```
## Using Fixtures in Tests
```typescript
import { test, expect } from './fixtures';
test('should show dashboard for logged in user', async ({ authenticatedPage }) => {
// authenticatedPage is already logged in
await expect(authenticatedPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
test('should create item via API', async ({ apiClient }) => {
const response = await apiClient.post('/api/items', {
data: { name: 'Test Item' },
});
expect(response.ok()).toBeTruthy();
});
```
## Shared Auth State (storageState)
For performance, authenticate once and reuse:
```typescript
// auth.setup.ts
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: '.auth/user.json' });
});
```
```typescript
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
storageState: '.auth/user.json',
},
dependencies: ['setup'],
},
],
});
```
## When to Use What
| Need | Use |
|---|---|
| Shared login state | `storageState` + setup project |
| Per-test data creation | Custom fixture with API calls |
| Reusable page helpers | Custom fixture returning page |
| Test data cleanup | Fixture teardown (after `use()`) |
| Config values | Simple value fixture |
FILE:reference/flaky-tests.md
# Flaky Test Quick Reference
## Diagnosis Commands
```bash
# Burn-in: expose timing issues
npx playwright test tests/checkout.spec.ts --repeat-each=10
# Isolation: expose state leaks
npx playwright test tests/checkout.spec.ts --grep "adds item" --workers=1
# Full trace: capture everything
npx playwright test tests/checkout.spec.ts --trace=on --retries=0
# Parallel stress: expose race conditions
npx playwright test --fully-parallel --workers=4 --repeat-each=5
```
## Four Categories
| Category | Symptom | Fix |
|---|---|---|
| **Timing** | Fails intermittently | Replace waits with assertions |
| **Isolation** | Fails in suite, passes alone | Remove shared state |
| **Environment** | Fails in CI only | Match viewport, fonts, timezone |
| **Infrastructure** | Random crashes | Reduce workers, increase memory |
## Quick Fixes
**Timing → Add proper waits:**
```typescript
// Wait for specific response
const response = page.waitForResponse('**/api/data');
await page.getByRole('button', { name: 'Load' }).click();
await response;
await expect(page.getByTestId('results')).toBeVisible();
```
**Isolation → Unique test data:**
```typescript
const uniqueEmail = `test-Date.now()@example.com`;
```
**Environment → Explicit viewport:**
```typescript
test.use({ viewport: { width: 1280, height: 720 } });
```
**Infrastructure → CI-safe config:**
```typescript
export default defineConfig({
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
timeout: process.env.CI ? 60_000 : 30_000,
});
```
FILE:reference/golden-rules.md
# Golden Rules
1. **`getByRole()` over CSS/XPath** — resilient to markup changes, mirrors assistive technology
2. **Never `page.waitForTimeout()`** — use `expect(locator).toBeVisible()` or `page.waitForURL()`
3. **Web-first assertions** — `expect(locator)` auto-retries; `expect(await locator.textContent())` does not
4. **Isolate every test** — no shared state, no execution-order dependencies
5. **`baseURL` in config** — zero hardcoded URLs in tests
6. **Retries: `2` in CI, `0` locally** — surface flakiness where it matters
7. **Traces: `'on-first-retry'`** — rich debugging artifacts without CI slowdown
8. **Fixtures over globals** — share state via `test.extend()`, not module-level variables
9. **One behavior per test** — multiple related `expect()` calls are fine
10. **Mock external services only** — never mock your own app; mock third-party APIs, payment gateways, email
FILE:reference/locators.md
# Locator Priority
Use the first option that works:
| Priority | Locator | Use for |
|---|---|---|
| 1 | `getByRole('button', { name: 'Submit' })` | Buttons, links, headings, form elements |
| 2 | `getByLabel('Email address')` | Form fields with associated labels |
| 3 | `getByText('Welcome back')` | Non-interactive text content |
| 4 | `getByPlaceholder('Search...')` | Inputs with placeholder text |
| 5 | `getByAltText('Company logo')` | Images with alt text |
| 6 | `getByTitle('Close dialog')` | Elements with title attribute |
| 7 | `getByTestId('checkout-summary')` | When no semantic option exists |
| 8 | `page.locator('.legacy-widget')` | CSS/XPath — absolute last resort |
## Role Locator Cheat Sheet
```typescript
// Buttons — <button>, <input type="submit">, [role="button"]
page.getByRole('button', { name: 'Save changes' })
// Links — <a href>
page.getByRole('link', { name: 'View profile' })
// Headings — h1-h6
page.getByRole('heading', { name: 'Dashboard', level: 1 })
// Text inputs — by label association
page.getByRole('textbox', { name: 'Email' })
// Checkboxes
page.getByRole('checkbox', { name: 'Remember me' })
// Radio buttons
page.getByRole('radio', { name: 'Monthly billing' })
// Dropdowns — <select>
page.getByRole('combobox', { name: 'Country' })
// Navigation
page.getByRole('navigation', { name: 'Main' })
// Tables
page.getByRole('table', { name: 'Recent orders' })
// Rows within tables
page.getByRole('row', { name: /Order #123/ })
// Tab panels
page.getByRole('tab', { name: 'Settings' })
// Dialogs
page.getByRole('dialog', { name: 'Confirm deletion' })
// Alerts
page.getByRole('alert')
```
## Filtering and Chaining
```typescript
// Filter by text
page.getByRole('listitem').filter({ hasText: 'Product A' })
// Filter by child locator
page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'Buy' })
})
// Chain locators
page.getByRole('navigation').getByRole('link', { name: 'Settings' })
// Nth match
page.getByRole('listitem').nth(0)
page.getByRole('listitem').first()
page.getByRole('listitem').last()
```
FILE:settings.json
{
"permissions": {
"allow": [
"Bash(npx playwright*)",
"Bash(npx tsx*)"
]
}
}
FILE:skills/browserstack/SKILL.md
---
name: "browserstack"
description: >-
Run tests on BrowserStack. Use when user mentions "browserstack",
"cross-browser", "cloud testing", "browser matrix", "test on safari",
"test on firefox", or "browser compatibility".
---
# BrowserStack Integration
Run Playwright tests on BrowserStack's cloud grid for cross-browser and cross-device testing.
## Prerequisites
Environment variables must be set:
- `BROWSERSTACK_USERNAME` — your BrowserStack username
- `BROWSERSTACK_ACCESS_KEY` — your access key
If not set, inform the user how to get them from [browserstack.com/accounts/settings](https://www.browserstack.com/accounts/settings) and stop.
## Capabilities
### 1. Configure for BrowserStack
```
/pw:browserstack setup
```
Steps:
1. Check current `playwright.config.ts`
2. Add BrowserStack connect options:
```typescript
// Add to playwright.config.ts
import { defineConfig } from '@playwright/test';
const isBS = !!process.env.BROWSERSTACK_USERNAME;
export default defineConfig({
// ... existing config
projects: isBS ? [
{
name: "chromelatestwindows-11",
use: {
connectOptions: {
wsEndpoint: `wss://cdp.browserstack.com/playwright?caps='chrome',
'browser_version': 'latest',
'os': 'Windows',
'os_version': '11',
'browserstack.username': process.env.BROWSERSTACK_USERNAME,
'browserstack.accessKey': process.env.BROWSERSTACK_ACCESS_KEY,))}`,
},
},
},
{
name: "firefoxlatestwindows-11",
use: {
connectOptions: {
wsEndpoint: `wss://cdp.browserstack.com/playwright?caps='playwright-firefox',
'browser_version': 'latest',
'os': 'Windows',
'os_version': '11',
'browserstack.username': process.env.BROWSERSTACK_USERNAME,
'browserstack.accessKey': process.env.BROWSERSTACK_ACCESS_KEY,))}`,
},
},
},
{
name: "webkitlatestos-x-ventura",
use: {
connectOptions: {
wsEndpoint: `wss://cdp.browserstack.com/playwright?caps='playwright-webkit',
'browser_version': 'latest',
'os': 'OS X',
'os_version': 'Ventura',
'browserstack.username': process.env.BROWSERSTACK_USERNAME,
'browserstack.accessKey': process.env.BROWSERSTACK_ACCESS_KEY,))}`,
},
},
},
] : [
// ... local projects fallback
],
});
```
3. Add npm script: `"test:e2e:cloud": "npx playwright test --project='chrome@*' --project='firefox@*' --project='webkit@*'"`
### 2. Run Tests on BrowserStack
```
/pw:browserstack run
```
Steps:
1. Verify credentials are set
2. Run tests with BrowserStack projects:
```bash
BROWSERSTACK_USERNAME=$BROWSERSTACK_USERNAME \
BROWSERSTACK_ACCESS_KEY=$BROWSERSTACK_ACCESS_KEY \
npx playwright test --project='chrome@*' --project='firefox@*'
```
3. Monitor execution
4. Report results per browser
### 3. Get Build Results
```
/pw:browserstack results
```
Steps:
1. Call `browserstack_get_builds` MCP tool
2. Get latest build's sessions
3. For each session:
- Status (pass/fail)
- Browser and OS
- Duration
- Video URL
- Log URLs
4. Format as summary table
### 4. Check Available Browsers
```
/pw:browserstack browsers
```
Steps:
1. Call `browserstack_get_browsers` MCP tool
2. Filter for Playwright-compatible browsers
3. Display available browser/OS combinations
### 5. Local Testing
```
/pw:browserstack local
```
For testing localhost or staging behind firewall:
1. Install BrowserStack Local: `npm install -D browserstack-local`
2. Add local tunnel to config
3. Provide setup instructions
## MCP Tools Used
| Tool | When |
|---|---|
| `browserstack_get_plan` | Check account limits |
| `browserstack_get_browsers` | List available browsers |
| `browserstack_get_builds` | List recent builds |
| `browserstack_get_sessions` | Get sessions in a build |
| `browserstack_get_session` | Get session details (video, logs) |
| `browserstack_update_session` | Mark pass/fail |
| `browserstack_get_logs` | Get text/network logs |
## Output
- Cross-browser test results table
- Per-browser pass/fail status
- Links to BrowserStack dashboard for video/screenshots
- Any browser-specific failures highlighted
FILE:skills/coverage/SKILL.md
---
name: "coverage"
description: >-
Analyze test coverage gaps. Use when user says "test coverage",
"what's not tested", "coverage gaps", "missing tests", "coverage report",
or "what needs testing".
---
# Analyze Test Coverage Gaps
Map all testable surfaces in the application and identify what's tested vs. what's missing.
## Steps
### 1. Map Application Surface
Use the `Explore` subagent to catalog:
**Routes/Pages:**
- Scan route definitions (Next.js `app/`, React Router config, Vue Router, etc.)
- List all user-facing pages with their paths
**Components:**
- Identify interactive components (forms, modals, dropdowns, tables)
- Note components with complex state logic
**API Endpoints:**
- Scan API route files or backend controllers
- List all endpoints with their methods
**User Flows:**
- Identify critical paths: auth, checkout, onboarding, core features
- Map multi-step workflows
### 2. Map Existing Tests
Scan all `*.spec.ts` / `*.spec.js` files:
- Extract which pages/routes are covered (by `page.goto()` calls)
- Extract which components are tested (by locator usage)
- Extract which API endpoints are mocked or hit
- Count tests per area
### 3. Generate Coverage Matrix
```
## Coverage Matrix
| Area | Route | Tests | Status |
|---|---|---|---|
| Auth | /login | 5 | ✅ Covered |
| Auth | /register | 0 | ❌ Missing |
| Auth | /forgot-password | 0 | ❌ Missing |
| Dashboard | /dashboard | 3 | ⚠️ Partial (no error states) |
| Settings | /settings | 0 | ❌ Missing |
| Checkout | /checkout | 8 | ✅ Covered |
```
### 4. Prioritize Gaps
Rank uncovered areas by business impact:
1. **Critical** — auth, payment, core features → test first
2. **High** — user-facing CRUD, search, navigation
3. **Medium** — settings, preferences, edge cases
4. **Low** — static pages, about, terms
### 5. Suggest Test Plan
For each gap, recommend:
- Number of tests needed
- Which template from `templates/` to use
- Estimated effort (quick/medium/complex)
```
## Recommended Test Plan
### Priority 1: Critical
1. /register (4 tests) — use auth/registration template — quick
2. /forgot-password (3 tests) — use auth/password-reset template — quick
### Priority 2: High
3. /settings (4 tests) — use settings/ templates — medium
4. Dashboard error states (2 tests) — use dashboard/data-loading template — quick
```
### 6. Auto-Generate (Optional)
Ask user: "Generate tests for the top N gaps? [Yes/No/Pick specific]"
If yes, invoke `/pw:generate` for each gap with the recommended template.
## Output
- Coverage matrix (table format)
- Coverage percentage estimate
- Prioritized gap list with effort estimates
- Option to auto-generate missing tests
FILE:skills/fix/SKILL.md
---
name: "fix"
description: >-
Fix failing or flaky Playwright tests. Use when user says "fix test",
"flaky test", "test failing", "debug test", "test broken", "test passes
sometimes", or "intermittent failure".
---
# Fix Failing or Flaky Tests
Diagnose and fix a Playwright test that fails or passes intermittently using a systematic taxonomy.
## Input
`$ARGUMENTS` contains:
- A test file path: `e2e/login.spec.ts`
- A test name: ""should redirect after login"`
- A description: `"the checkout test fails in CI but passes locally"`
## Steps
### 1. Reproduce the Failure
Run the test to capture the error:
```bash
npx playwright test <file> --reporter=list
```
If the test passes, it's likely flaky. Run burn-in:
```bash
npx playwright test <file> --repeat-each=10 --reporter=list
```
If it still passes, try with parallel workers:
```bash
npx playwright test --fully-parallel --workers=4 --repeat-each=5
```
### 2. Capture Trace
Run with full tracing:
```bash
npx playwright test <file> --trace=on --retries=0
```
Read the trace output. Use `/debug` to analyze trace files if available.
### 3. Categorize the Failure
Load `flaky-taxonomy.md` from this skill directory.
Every failing test falls into one of four categories:
| Category | Symptom | Diagnosis |
|---|---|---|
| **Timing/Async** | Fails intermittently everywhere | `--repeat-each=20` reproduces locally |
| **Test Isolation** | Fails in suite, passes alone | `--workers=1 --grep "test name"` passes |
| **Environment** | Fails in CI, passes locally | Compare CI vs local screenshots/traces |
| **Infrastructure** | Random, no pattern | Error references browser internals |
### 4. Apply Targeted Fix
**Timing/Async:**
- Replace `waitForTimeout()` with web-first assertions
- Add `await` to missing Playwright calls
- Wait for specific network responses before asserting
- Use `toBeVisible()` before interacting with elements
**Test Isolation:**
- Remove shared mutable state between tests
- Create test data per-test via API or fixtures
- Use unique identifiers (timestamps, random strings) for test data
- Check for database state leaks
**Environment:**
- Match viewport sizes between local and CI
- Account for font rendering differences in screenshots
- Use `docker` locally to match CI environment
- Check for timezone-dependent assertions
**Infrastructure:**
- Increase timeout for slow CI runners
- Add retries in CI config (`retries: 2`)
- Check for browser OOM (reduce parallel workers)
- Ensure browser dependencies are installed
### 5. Verify the Fix
Run the test 10 times to confirm stability:
```bash
npx playwright test <file> --repeat-each=10 --reporter=list
```
All 10 must pass. If any fail, go back to step 3.
### 6. Prevent Recurrence
Suggest:
- Add to CI with `retries: 2` if not already
- Enable `trace: 'on-first-retry'` in config
- Add the fix pattern to project's test conventions doc
## Output
- Root cause category and specific issue
- The fix applied (with diff)
- Verification result (10/10 passes)
- Prevention recommendation
FILE:skills/fix/flaky-taxonomy.md
# Flaky Test Taxonomy
## Decision Tree
```
Test is flaky
│
├── Fails locally with --repeat-each=20?
│ ├── YES → TIMING / ASYNC
│ │ ├── Missing await? → Add await
│ │ ├── waitForTimeout? → Replace with assertion
│ │ ├── Race condition? → Wait for specific event
│ │ └── Animation? → Wait for animation end or disable
│ │
│ └── NO → Continue...
│
├── Passes alone, fails in suite?
│ ├── YES → TEST ISOLATION
│ │ ├── Shared variable? → Make per-test
│ │ ├── Database state? → Reset per-test
│ │ ├── localStorage? → Clear in beforeEach
│ │ └── Cookie leak? → Use isolated contexts
│ │
│ └── NO → Continue...
│
├── Fails in CI, passes locally?
│ ├── YES → ENVIRONMENT
│ │ ├── Viewport? → Set explicit size
│ │ ├── Fonts? → Use Docker locally
│ │ ├── Timezone? → Use UTC everywhere
│ │ └── Network? → Mock external services
│ │
│ └── NO → INFRASTRUCTURE
│ ├── Browser crash? → Reduce workers
│ ├── OOM? → Limit parallel tests
│ ├── DNS? → Add retry config
│ └── File system? → Use unique temp dirs
```
## Common Fixes by Category
### Timing / Async
**Missing await:**
```typescript
// BAD — race condition
page.goto('/dashboard');
expect(page.getByText('Welcome')).toBeVisible();
// GOOD
await page.goto('/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();
```
**Clicking before visible:**
```typescript
// BAD — element may not be ready
await page.getByRole('button', { name: 'Submit' }).click();
// GOOD — ensure visible first
const submitBtn = page.getByRole('button', { name: 'Submit' });
await expect(submitBtn).toBeVisible();
await submitBtn.click();
```
**Race with network:**
```typescript
// BAD — data might not be loaded
await page.goto('/users');
await expect(page.getByRole('table')).toBeVisible();
// GOOD — wait for API response
const responsePromise = page.waitForResponse('**/api/users');
await page.goto('/users');
await responsePromise;
await expect(page.getByRole('table')).toBeVisible();
```
### Test Isolation
**Shared state fix:**
```typescript
// BAD — tests share userId
let userId: string;
test('create', async () => { userId = '123'; });
test('read', async () => { /* uses userId */ });
// GOOD — each test is independent
test('read user', async ({ request }) => {
const response = await request.post('/api/users', { data: { name: 'Test' } });
const { id } = await response.json();
// Use id within this test
});
```
**localStorage cleanup:**
```typescript
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.evaluate(() => localStorage.clear());
});
```
### Environment
**Explicit viewport:**
```typescript
test.use({ viewport: { width: 1280, height: 720 } });
```
**Timezone-safe dates:**
```typescript
// BAD
expect(dateText).toBe('March 5, 2026');
// GOOD — timezone independent
expect(dateText).toMatch(/\d{1,2}\/\d{1,2}\/\d{4}/);
```
### Infrastructure
**Retry config:**
```typescript
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
});
```
**Increase timeout for CI:**
```typescript
test.setTimeout(60_000); // 60s for slow CI
```
FILE:skills/generate/SKILL.md
---
name: "generate"
description: >-
Generate Playwright tests. Use when user says "write tests", "generate tests",
"add tests for", "test this component", "e2e test", "create test for",
"test this page", or "test this feature".
---
# Generate Playwright Tests
Generate production-ready Playwright tests from a user story, URL, component name, or feature description.
## Input
`$ARGUMENTS` contains what to test. Examples:
- `"user can log in with email and password"`
- `"the checkout flow"`
- `"src/components/UserProfile.tsx"`
- `"the search page with filters"`
## Steps
### 1. Understand the Target
Parse `$ARGUMENTS` to determine:
- **User story**: Extract the behavior to verify
- **Component path**: Read the component source code
- **Page/URL**: Identify the route and its elements
- **Feature name**: Map to relevant app areas
### 2. Explore the Codebase
Use the `Explore` subagent to gather context:
- Read `playwright.config.ts` for `testDir`, `baseURL`, `projects`
- Check existing tests in `testDir` for patterns, fixtures, and conventions
- If a component path is given, read the component to understand its props, states, and interactions
- Check for existing page objects in `pages/`
- Check for existing fixtures in `fixtures/`
- Check for auth setup (`auth.setup.ts` or `storageState` config)
### 3. Select Templates
Check `templates/` in this plugin for matching patterns:
| If testing... | Load template from |
|---|---|
| Login/auth flow | `templates/auth/login.md` |
| CRUD operations | `templates/crud/` |
| Checkout/payment | `templates/checkout/` |
| Search/filter UI | `templates/search/` |
| Form submission | `templates/forms/` |
| Dashboard/data | `templates/dashboard/` |
| Settings page | `templates/settings/` |
| Onboarding flow | `templates/onboarding/` |
| API endpoints | `templates/api/` |
| Accessibility | `templates/accessibility/` |
Adapt the template to the specific app — replace `{{placeholders}}` with actual selectors, URLs, and data.
### 4. Generate the Test
Follow these rules:
**Structure:**
```typescript
import { test, expect } from '@playwright/test';
// Import custom fixtures if the project uses them
test.describe('Feature Name', () => {
// Group related behaviors
test('should <expected behavior>', async ({ page }) => {
// Arrange: navigate, set up state
// Act: perform user action
// Assert: verify outcome
});
});
```
**Locator priority** (use the first that works):
1. `getByRole()` — buttons, links, headings, form elements
2. `getByLabel()` — form fields with labels
3. `getByText()` — non-interactive text content
4. `getByPlaceholder()` — inputs with placeholder text
5. `getByTestId()` — when semantic options aren't available
**Assertions** — always web-first:
```typescript
// GOOD — auto-retries
await expect(page.getByRole('heading')).toBeVisible();
await expect(page.getByRole('alert')).toHaveText('Success');
// BAD — no retry
const text = await page.textContent('.msg');
expect(text).toBe('Success');
```
**Never use:**
- `page.waitForTimeout()`
- `page.$(selector)` or `page.$$(selector)`
- Bare CSS selectors unless absolutely necessary
- `page.evaluate()` for things locators can do
**Always include:**
- Descriptive test names that explain the behavior
- Error/edge case tests alongside happy path
- Proper `await` on every Playwright call
- `baseURL`-relative navigation (`page.goto('/')` not `page.goto('http://...')`)
### 5. Match Project Conventions
- If project uses TypeScript → generate `.spec.ts`
- If project uses JavaScript → generate `.spec.js` with `require()` imports
- If project has page objects → use them instead of inline locators
- If project has custom fixtures → import and use them
- If project has a test data directory → create test data files there
### 6. Generate Supporting Files (If Needed)
- **Page object**: If the test touches 5+ unique locators on one page, create a page object
- **Fixture**: If the test needs shared setup (auth, data), create or extend a fixture
- **Test data**: If the test uses structured data, create a JSON file in `test-data/`
### 7. Verify
Run the generated test:
```bash
npx playwright test <generated-file> --reporter=list
```
If it fails:
1. Read the error
2. Fix the test (not the app)
3. Run again
4. If it's an app issue, report it to the user
## Output
- Generated test file(s) with path
- Any supporting files created (page objects, fixtures, data)
- Test run result
- Coverage note: what behaviors are now tested
FILE:skills/generate/patterns.md
# Test Generation Patterns
## Pattern: Authentication Flow
```typescript
test.describe('Authentication', () => {
test('should login with valid credentials', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('wrong');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('alert')).toHaveText(/invalid/i);
await expect(page).toHaveURL('/login');
});
});
```
## Pattern: CRUD Operations
```typescript
test.describe('Items', () => {
test('should create a new item', async ({ page }) => {
await page.goto('/items');
await page.getByRole('button', { name: 'Add item' }).click();
await page.getByLabel('Name').fill('Test Item');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Test Item')).toBeVisible();
});
test('should edit an existing item', async ({ page }) => {
await page.goto('/items');
await page.getByRole('row', { name: /Test Item/ })
.getByRole('button', { name: 'Edit' }).click();
await page.getByLabel('Name').clear();
await page.getByLabel('Name').fill('Updated Item');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Updated Item')).toBeVisible();
});
test('should delete an item with confirmation', async ({ page }) => {
await page.goto('/items');
await page.getByRole('row', { name: /Test Item/ })
.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByText('Test Item')).not.toBeVisible();
});
});
```
## Pattern: Form with Validation
```typescript
test.describe('Contact Form', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/contact');
});
test('should submit valid form', async ({ page }) => {
await page.getByLabel('Name').fill('Jane Doe');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Message').fill('Hello, this is a test message.');
await page.getByRole('button', { name: 'Send' }).click();
await expect(page.getByText('Message sent')).toBeVisible();
});
test('should show validation errors for empty required fields', async ({ page }) => {
await page.getByRole('button', { name: 'Send' }).click();
await expect(page.getByText('Name is required')).toBeVisible();
await expect(page.getByText('Email is required')).toBeVisible();
});
test('should validate email format', async ({ page }) => {
await page.getByLabel('Email').fill('not-an-email');
await page.getByRole('button', { name: 'Send' }).click();
await expect(page.getByText('Invalid email')).toBeVisible();
});
});
```
## Pattern: Search and Filter
```typescript
test.describe('Product Search', () => {
test('should return results for valid query', async ({ page }) => {
await page.goto('/products');
await page.getByPlaceholder('Search products').fill('laptop');
await page.getByRole('button', { name: 'Search' }).click();
await expect(page.getByRole('list')).toBeVisible();
const results = page.getByRole('listitem');
await expect(results).not.toHaveCount(0);
});
test('should show empty state for no results', async ({ page }) => {
await page.goto('/products');
await page.getByPlaceholder('Search products').fill('xyznonexistent');
await page.getByRole('button', { name: 'Search' }).click();
await expect(page.getByText('No products found')).toBeVisible();
});
test('should filter by category', async ({ page }) => {
await page.goto('/products');
await page.getByRole('combobox', { name: 'Category' }).selectOption('Electronics');
await expect(page.getByRole('listitem')).not.toHaveCount(0);
});
});
```
## Pattern: Navigation and Layout
```typescript
test.describe('Navigation', () => {
test('should navigate between pages', async ({ page }) => {
await page.goto('/');
await page.getByRole('link', { name: 'About' }).click();
await expect(page).toHaveURL('/about');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('About');
});
test('should show mobile menu on small screens', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/');
await expect(page.getByRole('navigation')).not.toBeVisible();
await page.getByRole('button', { name: 'Menu' }).click();
await expect(page.getByRole('navigation')).toBeVisible();
});
});
```
## Pattern: API Mocking
```typescript
test.describe('Dashboard with mocked API', () => {
test('should display data from API', async ({ page }) => {
await page.route('**/api/dashboard', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ revenue: 50000, users: 1200 }),
});
});
await page.goto('/dashboard');
await expect(page.getByText('$50,000')).toBeVisible();
await expect(page.getByText('1,200')).toBeVisible();
});
test('should handle API errors gracefully', async ({ page }) => {
await page.route('**/api/dashboard', (route) => {
route.fulfill({ status: 500 });
});
await page.goto('/dashboard');
await expect(page.getByText(/error|try again/i)).toBeVisible();
});
});
```
FILE:skills/init/SKILL.md
---
name: "init"
description: >-
Set up Playwright in a project. Use when user says "set up playwright",
"add e2e tests", "configure playwright", "testing setup", "init playwright",
or "add test infrastructure".
---
# Initialize Playwright Project
Set up a production-ready Playwright testing environment. Detect the framework, generate config, folder structure, example test, and CI workflow.
## Steps
### 1. Analyze the Project
Use the `Explore` subagent to scan the project:
- Check `package.json` for framework (React, Next.js, Vue, Angular, Svelte)
- Check for `tsconfig.json` → use TypeScript; otherwise JavaScript
- Check if Playwright is already installed (`@playwright/test` in dependencies)
- Check for existing test directories (`tests/`, `e2e/`, `__tests__/`)
- Check for existing CI config (`.github/workflows/`, `.gitlab-ci.yml`)
### 2. Install Playwright
If not already installed:
```bash
npm init playwright@latest -- --quiet
```
Or if the user prefers manual setup:
```bash
npm install -D @playwright/test
npx playwright install --with-deps chromium
```
### 3. Generate `playwright.config.ts`
Adapt to the detected framework:
**Next.js:**
```typescript
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { open: 'never' }],
['list'],
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: "chromium", use: { ...devices['Desktop Chrome'] } },
{ name: "firefox", use: { ...devices['Desktop Firefox'] } },
{ name: "webkit", use: { ...devices['Desktop Safari'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
```
**React (Vite):**
- Change `baseURL` to `http://localhost:5173`
- Change `webServer.command` to `npm run dev`
**Vue/Nuxt:**
- Change `baseURL` to `http://localhost:3000`
- Change `webServer.command` to `npm run dev`
**Angular:**
- Change `baseURL` to `http://localhost:4200`
- Change `webServer.command` to `npm run start`
**No framework detected:**
- Omit `webServer` block
- Set `baseURL` from user input or leave as placeholder
### 4. Create Folder Structure
```
e2e/
├── fixtures/
│ └── index.ts # Custom fixtures
├── pages/
│ └── .gitkeep # Page object models
├── test-data/
│ └── .gitkeep # Test data files
└── example.spec.ts # First example test
```
### 5. Generate Example Test
```typescript
import { test, expect } from '@playwright/test';
test.describe('Homepage', () => {
test('should load successfully', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/.+/);
});
test('should have visible navigation', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('navigation')).toBeVisible();
});
});
```
### 6. Generate CI Workflow
If `.github/workflows/` exists, create `playwright.yml`:
```yaml
name: "playwright-tests"
on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: "install-dependencies"
run: npm ci
- name: "install-playwright-browsers"
run: npx playwright install --with-deps
- name: "run-playwright-tests"
run: npx playwright test
- uses: actions/upload-artifact@v4
if: { !cancelled()}
with:
name: "playwright-report"
path: playwright-report/
retention-days: 30
```
If `.gitlab-ci.yml` exists, add a Playwright stage instead.
### 7. Update `.gitignore`
Append if not already present:
```
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
```
### 8. Add npm Scripts
Add to `package.json` scripts:
```json
{
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
}
```
### 9. Verify Setup
Run the example test:
```bash
npx playwright test
```
Report the result. If it fails, diagnose and fix before completing.
## Output
Confirm what was created:
- Config file path and key settings
- Test directory and example test
- CI workflow (if applicable)
- npm scripts added
- How to run: `npx playwright test` or `npm run test:e2e`
FILE:skills/migrate/SKILL.md
---
name: "migrate"
description: >-
Migrate from Cypress or Selenium to Playwright. Use when user mentions
"cypress", "selenium", "migrate tests", "convert tests", "switch to
playwright", "move from cypress", or "replace selenium".
---
# Migrate to Playwright
Interactive migration from Cypress or Selenium to Playwright with file-by-file conversion.
## Input
`$ARGUMENTS` can be:
- `"from cypress"` — migrate Cypress test suite
- `"from selenium"` — migrate Selenium/WebDriver tests
- A file path: convert a specific test file
- Empty: auto-detect source framework
## Steps
### 1. Detect Source Framework
Use `Explore` subagent to scan:
- `cypress/` directory or `cypress.config.ts` → Cypress
- `selenium`, `webdriver` in `package.json` deps → Selenium
- `.py` test files with `selenium` imports → Selenium (Python)
### 2. Assess Migration Scope
Count files and categorize:
```
Migration Assessment:
- Total test files: X
- Cypress custom commands: Y
- Cypress fixtures: Z
- Estimated effort: [small|medium|large]
```
| Size | Files | Approach |
|---|---|---|
| Small (1-10) | Convert sequentially | Direct conversion |
| Medium (11-30) | Batch in groups of 5 | Use sub-agents |
| Large (31+) | Use `/batch` | Parallel conversion with `/batch` |
### 3. Set Up Playwright (If Not Present)
Run `/pw:init` first if Playwright isn't configured.
### 4. Convert Files
For each file, apply the appropriate mapping:
#### Cypress → Playwright
Load `cypress-mapping.md` for complete reference.
Key translations:
```
cy.visit(url) → page.goto(url)
cy.get(selector) → page.locator(selector) or page.getByRole(...)
cy.contains(text) → page.getByText(text)
cy.find(selector) → locator.locator(selector)
cy.click() → locator.click()
cy.type(text) → locator.fill(text)
cy.should('be.visible') → expect(locator).toBeVisible()
cy.should('have.text') → expect(locator).toHaveText(text)
cy.intercept() → page.route()
cy.wait('@alias') → page.waitForResponse()
cy.fixture() → JSON import or test data file
```
**Cypress custom commands** → Playwright fixtures or helper functions
**Cypress plugins** → Playwright config or fixtures
**`before`/`beforeEach`** → `test.beforeAll()` / `test.beforeEach()`
#### Selenium → Playwright
Load `selenium-mapping.md` for complete reference.
Key translations:
```
driver.get(url) → page.goto(url)
driver.findElement(By.id('x')) → page.locator('#x') or page.getByTestId('x')
driver.findElement(By.css('.x')) → page.locator('.x') or page.getByRole(...)
element.click() → locator.click()
element.sendKeys(text) → locator.fill(text)
element.getText() → locator.textContent()
WebDriverWait + ExpectedConditions → expect(locator).toBeVisible()
driver.switchTo().frame() → page.frameLocator()
Actions → locator.hover(), locator.dragTo()
```
### 5. Upgrade Locators
During conversion, upgrade selectors to Playwright best practices:
- `#id` → `getByTestId()` or `getByRole()`
- `.class` → `getByRole()` or `getByText()`
- `[data-testid]` → `getByTestId()`
- XPath → role-based locators
### 6. Convert Custom Commands / Utilities
- Cypress custom commands → Playwright custom fixtures via `test.extend()`
- Selenium page objects → Playwright page objects (keep structure, update API)
- Shared helpers → TypeScript utility functions
### 7. Verify Each Converted File
After converting each file:
```bash
npx playwright test <converted-file> --reporter=list
```
Fix any compilation or runtime errors before moving to the next file.
### 8. Clean Up
After all files are converted:
- Remove Cypress/Selenium dependencies from `package.json`
- Remove old config files (`cypress.config.ts`, etc.)
- Update CI workflow to use Playwright
- Update README with new test commands
Ask user before deleting anything.
## Output
- Conversion summary: files converted, total tests migrated
- Any tests that couldn't be auto-converted (manual intervention needed)
- Updated CI config
- Before/after comparison of test run results
FILE:skills/migrate/cypress-mapping.md
# Cypress → Playwright Mapping
## Commands
| Cypress | Playwright | Notes |
|---|---|---|
| `cy.visit('/page')` | `await page.goto('/page')` | Use `baseURL` in config |
| `cy.get('.selector')` | `page.locator('.selector')` | Prefer `getByRole()` |
| `cy.get('[data-cy=x]')` | `page.getByTestId('x')` | |
| `cy.contains('text')` | `page.getByText('text')` | |
| `cy.find('.child')` | `parent.locator('.child')` | Chain from parent locator |
| `cy.first()` | `locator.first()` | |
| `cy.last()` | `locator.last()` | |
| `cy.eq(n)` | `locator.nth(n)` | |
| `cy.parent()` | `locator.locator('..')` | Or restructure with better locators |
| `cy.children()` | `locator.locator('> *')` | |
| `cy.siblings()` | Not direct — restructure test | Use parent + filter |
## Actions
| Cypress | Playwright | Notes |
|---|---|---|
| `.click()` | `await locator.click()` | Always `await` |
| `.dblclick()` | `await locator.dblclick()` | |
| `.rightclick()` | `await locator.click({ button: 'right' })` | |
| `.type('text')` | `await locator.fill('text')` | `fill()` clears first |
| `.type('text', { delay: 50 })` | `await locator.pressSequentially('text', { delay: 50 })` | Simulates typing |
| `.clear()` | `await locator.clear()` | |
| `.check()` | `await locator.check()` | |
| `.uncheck()` | `await locator.uncheck()` | |
| `.select('value')` | `await locator.selectOption('value')` | |
| `.scrollTo()` | `await locator.scrollIntoViewIfNeeded()` | |
| `.trigger('event')` | `await locator.dispatchEvent('event')` | |
| `.focus()` | `await locator.focus()` | |
| `.blur()` | `await locator.blur()` | |
## Assertions
| Cypress | Playwright | Notes |
|---|---|---|
| `.should('be.visible')` | `await expect(locator).toBeVisible()` | Web-first, auto-retry |
| `.should('not.exist')` | `await expect(locator).not.toBeVisible()` | Or `.toHaveCount(0)` |
| `.should('have.text', 'x')` | `await expect(locator).toHaveText('x')` | |
| `.should('contain', 'x')` | `await expect(locator).toContainText('x')` | |
| `.should('have.value', 'x')` | `await expect(locator).toHaveValue('x')` | |
| `.should('have.attr', 'x', 'y')` | `await expect(locator).toHaveAttribute('x', 'y')` | |
| `.should('have.class', 'x')` | `await expect(locator).toHaveClass(/x/)` | |
| `.should('be.disabled')` | `await expect(locator).toBeDisabled()` | |
| `.should('be.checked')` | `await expect(locator).toBeChecked()` | |
| `.should('have.length', n)` | `await expect(locator).toHaveCount(n)` | |
| `cy.url().should('include', '/x')` | `await expect(page).toHaveURL(/\/x/)` | |
| `cy.title().should('eq', 'x')` | `await expect(page).toHaveTitle('x')` | |
## Network
| Cypress | Playwright |
|---|---|
| `cy.intercept('GET', '/api/*', { body: data })` | `await page.route('**/api/*', route => route.fulfill({ body: JSON.stringify(data) }))` |
| `cy.intercept('POST', '/api/*').as('save')` | `const savePromise = page.waitForResponse('**/api/*')` |
| `cy.wait('@save')` | `await savePromise` |
## Fixtures & Custom Commands
| Cypress | Playwright |
|---|---|
| `cy.fixture('data.json')` | `import data from './test-data/data.json'` |
| `Cypress.Commands.add('login', ...)` | `test.extend({ authenticatedPage: ... })` |
| `beforeEach(() => { ... })` | `test.beforeEach(async ({ page }) => { ... })` |
| `before(() => { ... })` | `test.beforeAll(async () => { ... })` |
## Config
| Cypress | Playwright |
|---|---|
| `baseUrl` in `cypress.config.ts` | `use.baseURL` in `playwright.config.ts` |
| `defaultCommandTimeout` | `expect.timeout` or `use.actionTimeout` |
| `video: true` | `use.video: 'on'` |
| `screenshotOnRunFailure` | `use.screenshot: 'only-on-failure'` |
| `retries: { runMode: 2 }` | `retries: 2` |
FILE:skills/migrate/selenium-mapping.md
# Selenium → Playwright Mapping
## Driver Setup
| Selenium (JS) | Playwright |
|---|---|
| `new Builder().forBrowser('chrome').build()` | Handled by config — no driver setup |
| `driver.quit()` | Automatic — Playwright manages browser lifecycle |
| `driver.manage().setTimeouts(...)` | Config: `timeout`, `expect.timeout` |
## Navigation
| Selenium | Playwright | Notes |
|---|---|---|
| `driver.get(url)` | `await page.goto(url)` | Use `baseURL` |
| `driver.navigate().back()` | `await page.goBack()` | |
| `driver.navigate().forward()` | `await page.goForward()` | |
| `driver.navigate().refresh()` | `await page.reload()` | |
| `driver.getCurrentUrl()` | `page.url()` | |
| `driver.getTitle()` | `await page.title()` | |
## Element Location
| Selenium | Playwright | Preferred |
|---|---|---|
| `By.id('x')` | `page.locator('#x')` | `page.getByTestId('x')` |
| `By.css('.x')` | `page.locator('.x')` | `page.getByRole(...)` |
| `By.xpath('//div')` | `page.locator('xpath=//div')` | Avoid — use role-based |
| `By.name('x')` | `page.locator('[name=x]')` | `page.getByLabel(...)` |
| `By.linkText('x')` | `page.getByRole('link', { name: 'x' })` | ✅ Best practice |
| `By.partialLinkText('x')` | `page.getByRole('link', { name: /x/ })` | ✅ Best practice |
| `By.tagName('button')` | `page.getByRole('button')` | ✅ Best practice |
| `By.className('x')` | `page.locator('.x')` | `page.getByRole(...)` |
| `findElement()` | Returns first match | `locator.first()` |
| `findElements()` | `page.locator(selector)` | Use `.count()` or `.all()` |
## Actions
| Selenium | Playwright |
|---|---|
| `element.click()` | `await locator.click()` |
| `element.sendKeys('text')` | `await locator.fill('text')` |
| `element.sendKeys(Key.ENTER)` | `await locator.press('Enter')` |
| `element.clear()` | `await locator.clear()` |
| `element.submit()` | `await locator.press('Enter')` or click submit button |
| `element.getText()` | `await locator.textContent()` |
| `element.getAttribute('x')` | `await locator.getAttribute('x')` |
| `element.isDisplayed()` | `await locator.isVisible()` |
| `element.isEnabled()` | `await locator.isEnabled()` |
| `element.isSelected()` | `await locator.isChecked()` |
## Waits
| Selenium | Playwright | Notes |
|---|---|---|
| `WebDriverWait(driver, 10).until(EC.visibilityOf(el))` | `await expect(locator).toBeVisible()` | Auto-retries |
| `WebDriverWait(driver, 10).until(EC.elementToBeClickable(el))` | `await locator.click()` | Auto-waits for clickable |
| `WebDriverWait(driver, 10).until(EC.presenceOf(el))` | `await expect(locator).toBeAttached()` | |
| `WebDriverWait(driver, 10).until(EC.textToBe(el, 'x'))` | `await expect(locator).toHaveText('x')` | |
| `Thread.sleep(3000)` | ❌ Never use | Use assertions instead |
| `driver.manage().setTimeouts({ implicit: 10000 })` | Not needed | Playwright auto-waits |
## Advanced
| Selenium | Playwright |
|---|---|
| `Actions(driver).moveToElement(el).perform()` | `await locator.hover()` |
| `Actions(driver).dragAndDrop(src, tgt).perform()` | `await src.dragTo(tgt)` |
| `Actions(driver).doubleClick(el).perform()` | `await locator.dblclick()` |
| `Actions(driver).contextClick(el).perform()` | `await locator.click({ button: 'right' })` |
| `driver.switchTo().frame(el)` | `page.frameLocator('#frame')` |
| `driver.switchTo().defaultContent()` | Not needed — use `page` directly |
| `driver.switchTo().alert()` | `page.on('dialog', d => d.accept())` |
| `driver.switchTo().window(handle)` | `const popup = await page.waitForEvent('popup')` |
| `driver.executeScript(js)` | `await page.evaluate(js)` |
| `driver.takeScreenshot()` | `await page.screenshot({ path: 'x.png' })` |
## Test Structure
| Selenium (Jest/Mocha) | Playwright |
|---|---|
| `describe('Suite', () => { ... })` | `test.describe('Suite', () => { ... })` |
| `it('should...', () => { ... })` | `test('should...', async ({ page }) => { ... })` |
| `beforeAll(() => { ... })` | `test.beforeAll(async () => { ... })` |
| `beforeEach(() => { ... })` | `test.beforeEach(async ({ page }) => { ... })` |
| `afterEach(() => { ... })` | `test.afterEach(async ({ page }) => { ... })` |
## Key Differences
1. **No implicit waits** — Playwright auto-waits for actionability
2. **No driver management** — Playwright handles browser lifecycle
3. **Built-in assertions** — `expect(locator)` with auto-retry
4. **Parallel by default** — tests run in parallel, must be isolated
5. **Traces instead of screenshots** — richer debugging artifacts
FILE:skills/report/SKILL.md
---
name: "report"
description: >-
Generate test report. Use when user says "test report", "results summary",
"test status", "show results", "test dashboard", or "how did tests go".
---
# Smart Test Reporting
Generate test reports that plug into the user's existing workflow. Zero new tools.
## Steps
### 1. Run Tests (If Not Already Run)
Check if recent test results exist:
```bash
ls -la test-results/ playwright-report/ 2>/dev/null
```
If no recent results, run tests:
```bash
npx playwright test --reporter=json,html,list 2>&1 | tee test-output.log
```
### 2. Parse Results
Read the JSON report:
```bash
npx playwright test --reporter=json 2> /dev/null
```
Extract:
- Total tests, passed, failed, skipped, flaky
- Duration per test and total
- Failed test names with error messages
- Flaky tests (passed on retry)
### 3. Detect Report Destination
Check what's configured and route automatically:
| Check | If found | Action |
|---|---|---|
| `TESTRAIL_URL` env var | TestRail configured | Push results via `/pw:testrail push` |
| `SLACK_WEBHOOK_URL` env var | Slack configured | Post summary to Slack |
| `.github/workflows/` | GitHub Actions | Results go to PR comment via artifacts |
| `playwright-report/` | HTML reporter | Open or serve the report |
| None of the above | Default | Generate markdown report |
### 4. Generate Report
#### Markdown Report (Always Generated)
```markdown
# Test Results — {{date}}
## Summary
- ✅ Passed: {{passed}}
- ❌ Failed: {{failed}}
- ⏭️ Skipped: {{skipped}}
- 🔄 Flaky: {{flaky}}
- ⏱️ Duration: {{duration}}
## Failed Tests
| Test | Error | File |
|---|---|---|
| {{name}} | {{error}} | {{file}}:{{line}} |
## Flaky Tests
| Test | Retries | File |
|---|---|---|
| {{name}} | {{retries}} | {{file}} |
## By Project
| Browser | Passed | Failed | Duration |
|---|---|---|---|
| Chromium | X | Y | Zs |
| Firefox | X | Y | Zs |
| WebKit | X | Y | Zs |
```
Save to `test-reports/{{date}}-report.md`.
#### Slack Summary (If Webhook Configured)
```bash
curl -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{
"text": "🧪 Test Results: ✅ {{passed}} | ❌ {{failed}} | ⏱️ {{duration}}\n{{failed_details}}"
}'
```
#### TestRail Push (If Configured)
Invoke `/pw:testrail push` with the JSON results.
#### HTML Report
```bash
npx playwright show-report
```
Or if in CI:
```bash
echo "HTML report available at: playwright-report/index.html"
```
### 5. Trend Analysis (If Historical Data Exists)
If previous reports exist in `test-reports/`:
- Compare pass rate over time
- Identify tests that became flaky recently
- Highlight new failures vs. recurring failures
## Output
- Summary with pass/fail/skip/flaky counts
- Failed test details with error messages
- Report destination confirmation
- Trend comparison (if historical data available)
- Next action recommendation (fix failures or celebrate green)
FILE:skills/review/SKILL.md
---
name: "review"
description: >-
Review Playwright tests for quality. Use when user says "review tests",
"check test quality", "audit tests", "improve tests", "test code review",
or "playwright best practices check".
---
# Review Playwright Tests
Systematically review Playwright test files for anti-patterns, missed best practices, and coverage gaps.
## Input
`$ARGUMENTS` can be:
- A file path: review that specific test file
- A directory: review all test files in the directory
- Empty: review all tests in the project's `testDir`
## Steps
### 1. Gather Context
- Read `playwright.config.ts` for project settings
- List all `*.spec.ts` / `*.spec.js` files in scope
- If reviewing a single file, also check related page objects and fixtures
### 2. Check Each File Against Anti-Patterns
Load `anti-patterns.md` from this skill directory. Check for all 20 anti-patterns.
**Critical (must fix):**
1. `waitForTimeout()` usage
2. Non-web-first assertions (`expect(await ...)`)
3. Hardcoded URLs instead of `baseURL`
4. CSS/XPath selectors when role-based exists
5. Missing `await` on Playwright calls
6. Shared mutable state between tests
7. Test execution order dependencies
**Warning (should fix):**
8. Tests longer than 50 lines (consider splitting)
9. Magic strings without named constants
10. Missing error/edge case tests
11. `page.evaluate()` for things locators can do
12. Nested `test.describe()` more than 2 levels deep
13. Generic test names ("should work", "test 1")
**Info (consider):**
14. No page objects for pages with 5+ locators
15. Inline test data instead of factory/fixture
16. Missing accessibility assertions
17. No visual regression tests for UI-heavy pages
18. Console error assertions not checked
19. Network idle waits instead of specific assertions
20. Missing `test.describe()` grouping
### 3. Score Each File
Rate 1-10 based on:
- **9-10**: Production-ready, follows all golden rules
- **7-8**: Good, minor improvements possible
- **5-6**: Functional but has anti-patterns
- **3-4**: Significant issues, likely flaky
- **1-2**: Needs rewrite
### 4. Generate Review Report
For each file:
```
## <filename> — Score: X/10
### Critical
- Line 15: `waitForTimeout(2000)` → use `expect(locator).toBeVisible()`
- Line 28: CSS selector `.btn-submit` → `getByRole('button', { name: "submit" })`
### Warning
- Line 42: Test name "test login" → "should redirect to dashboard after login"
### Suggestions
- Consider adding error case: what happens with invalid credentials?
```
### 5. For Project-Wide Review
If reviewing an entire test suite:
- Spawn sub-agents per file for parallel review (up to 5 concurrent)
- Or use `/batch` for very large suites
- Aggregate results into a summary table
### 6. Offer Fixes
For each critical issue, provide the corrected code. Ask user: "Apply these fixes? [Yes/No]"
If yes, apply all fixes using `Edit` tool.
## Output
- File-by-file review with scores
- Summary: total files, average score, critical issue count
- Actionable fix list
- Coverage gaps identified (pages/features with no tests)
FILE:skills/review/anti-patterns.md
# Playwright Anti-Patterns Reference
## 1. Using `waitForTimeout()`
**Bad:**
```typescript
await page.click('.submit');
await page.waitForTimeout(3000);
await expect(page.locator('.result')).toBeVisible();
```
**Good:**
```typescript
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByTestId('result')).toBeVisible();
```
**Why:** Arbitrary waits slow tests and cause flakiness. Web-first assertions auto-retry.
## 2. Non-Web-First Assertions
**Bad:**
```typescript
const text = await page.textContent('.message');
expect(text).toBe('Success');
```
**Good:**
```typescript
await expect(page.getByText('Success')).toBeVisible();
```
**Why:** `expect(locator)` auto-retries until timeout. `expect(value)` checks once and fails.
## 3. Hardcoded URLs
**Bad:**
```typescript
await page.goto('http://localhost:3000/login');
```
**Good:**
```typescript
await page.goto('/login');
```
**Why:** `baseURL` in config handles the host. Tests break across environments with hardcoded URLs.
## 4. CSS/XPath When Role-Based Exists
**Bad:**
```typescript
await page.click('#submit-btn');
await page.locator('.nav-link.active').click();
```
**Good:**
```typescript
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('link', { name: 'Dashboard' }).click();
```
**Why:** Role-based locators survive CSS renames, class refactors, and component library changes.
## 5. Missing `await`
**Bad:**
```typescript
page.goto('/dashboard');
expect(page.getByText('Welcome')).toBeVisible();
```
**Good:**
```typescript
await page.goto('/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();
```
**Why:** Missing `await` causes race conditions. Tests pass sometimes, fail others.
## 6. Shared Mutable State
**Bad:**
```typescript
let userId: string;
test('create user', async ({ page }) => {
// ... creates user, sets userId
userId = '123';
});
test('edit user', async ({ page }) => {
await page.goto(`/users/userId`); // depends on previous test
});
```
**Good:**
```typescript
test('edit user', async ({ page }) => {
// Create user via API in this test's setup
const userId = await createUserViaAPI();
await page.goto(`/users/userId`);
});
```
**Why:** Tests must be independent. Shared state causes order-dependent failures.
## 7. Execution Order Dependencies
**Bad:**
```typescript
test('step 1: fill form', async ({ page }) => { ... });
test('step 2: submit form', async ({ page }) => { ... });
test('step 3: verify result', async ({ page }) => { ... });
```
**Good:**
```typescript
test('should fill and submit form successfully', async ({ page }) => {
// All steps in one test
});
```
**Why:** Playwright runs tests in parallel by default. Order-dependent tests fail randomly.
## 8. Tests Over 50 Lines
Split into focused tests. Each test should verify one behavior.
## 9. Magic Strings
**Bad:**
```typescript
await page.getByLabel('Email').fill('[email protected]');
```
**Good:**
```typescript
const TEST_USER = { email: '[email protected]', password: 'Test123!' };
await page.getByLabel('Email').fill(TEST_USER.email);
```
## 10. Missing Error Cases
If you test the happy path, also test:
- Invalid input
- Empty state
- Network error
- Permission denied
- Timeout/loading state
## 11. Using `page.evaluate()` Unnecessarily
**Bad:**
```typescript
const text = await page.evaluate(() => document.querySelector('.title')?.textContent);
```
**Good:**
```typescript
await expect(page.getByRole('heading')).toHaveText('Expected Title');
```
## 12. Deep Nesting
Keep `test.describe()` to max 2 levels. More makes tests hard to find and maintain.
## 13. Generic Test Names
**Bad:** `test('test 1')`, `test('should work')`, `test('login test')`
**Good:** `test('should show error when email is invalid')`, `test('should redirect to dashboard after successful login')`
## 14-20. Style Issues
- No page objects for complex pages → create them
- Inline data → use factories or fixtures
- Missing a11y assertions → add `toHaveAttribute('role', ...)`
- No visual regression → add `toHaveScreenshot()` for key pages
- Not checking console errors → add `page.on('console', ...)`
- Using `networkidle` → use specific assertions instead
- No `test.describe()` → group related tests
FILE:skills/testrail/SKILL.md
---
name: "testrail"
description: >-
Sync tests with TestRail. Use when user mentions "testrail", "test management",
"test cases", "test run", "sync test cases", "push results to testrail",
or "import from testrail".
---
# TestRail Integration
Bidirectional sync between Playwright tests and TestRail test management.
## Prerequisites
Environment variables must be set:
- `TESTRAIL_URL` — e.g., `https://your-instance.testrail.io`
- `TESTRAIL_USER` — your email
- `TESTRAIL_API_KEY` — API key from TestRail
If not set, inform the user how to configure them and stop.
## Capabilities
### 1. Import Test Cases → Generate Playwright Tests
```
/pw:testrail import --project <id> --suite <id>
```
Steps:
1. Call `testrail_get_cases` MCP tool to fetch test cases
2. For each test case:
- Read title, preconditions, steps, expected results
- Map to a Playwright test using appropriate template
- Include TestRail case ID as test annotation: `test.info().annotations.push({ type: 'testrail', description: 'C12345' })`
3. Generate test files grouped by section
4. Report: X cases imported, Y tests generated
### 2. Push Test Results → TestRail
```
/pw:testrail push --run <id>
```
Steps:
1. Run Playwright tests with JSON reporter:
```bash
npx playwright test --reporter=json > test-results.json
```
2. Parse results: map each test to its TestRail case ID (from annotations)
3. Call `testrail_add_result` MCP tool for each test:
- Pass → status_id: 1
- Fail → status_id: 5, include error message
- Skip → status_id: 2
4. Report: X results pushed, Y passed, Z failed
### 3. Create Test Run
```
/pw:testrail run --project <id> --name "Sprint 42 Regression"
```
Steps:
1. Call `testrail_add_run` MCP tool
2. Include all test case IDs found in Playwright test annotations
3. Return run ID for result pushing
### 4. Sync Status
```
/pw:testrail status --project <id>
```
Steps:
1. Fetch test cases from TestRail
2. Scan local Playwright tests for TestRail annotations
3. Report coverage:
```
TestRail cases: 150
Playwright tests with TestRail IDs: 120
Unlinked TestRail cases: 30
Playwright tests without TestRail IDs: 15
```
### 5. Update Test Cases in TestRail
```
/pw:testrail update --case <id>
```
Steps:
1. Read the Playwright test for this case ID
2. Extract steps and expected results from test code
3. Call `testrail_update_case` MCP tool to update steps
## MCP Tools Used
| Tool | When |
|---|---|
| `testrail_get_projects` | List available projects |
| `testrail_get_suites` | List suites in project |
| `testrail_get_cases` | Read test cases |
| `testrail_add_case` | Create new test case |
| `testrail_update_case` | Update existing case |
| `testrail_add_run` | Create test run |
| `testrail_add_result` | Push individual result |
| `testrail_get_results` | Read historical results |
## Test Annotation Format
All Playwright tests linked to TestRail include:
```typescript
test('should login successfully', async ({ page }) => {
test.info().annotations.push({
type: 'testrail',
description: 'C12345',
});
// ... test code
});
```
This annotation is the bridge between Playwright and TestRail.
## Output
- Operation summary with counts
- Any errors or unmatched cases
- Link to TestRail run/results
FILE:templates/README.md
# Test Case Templates
55 ready-to-use, parametrizable Playwright test templates. Each includes TypeScript and JavaScript examples with `{{placeholder}}` markers for customization.
## Usage
Templates are loaded by `/pw:generate` when it detects a matching scenario. You can also reference them directly:
```
/pw:generate "login flow" → loads templates/auth/login.md
```
## Template Index
### Authentication (8)
| Template | Tests |
|---|---|
| [login.md](auth/login.md) | Email/password login, social login, remember me |
| [logout.md](auth/logout.md) | Logout from nav, session cleanup |
| [sso.md](auth/sso.md) | SSO redirect flow, callback handling |
| [mfa.md](auth/mfa.md) | 2FA code entry, backup codes |
| [password-reset.md](auth/password-reset.md) | Request reset, enter new password, expired link |
| [session-timeout.md](auth/session-timeout.md) | Auto-logout, session refresh |
| [remember-me.md](auth/remember-me.md) | Persistent login, cookie expiry |
| [rbac.md](auth/rbac.md) | Role-based access, forbidden page |
### CRUD Operations (6)
| Template | Tests |
|---|---|
| [create.md](crud/create.md) | Create entity with form |
| [read.md](crud/read.md) | View details, list view |
| [update.md](crud/update.md) | Edit entity, inline edit |
| [delete.md](crud/delete.md) | Delete with confirmation |
| [bulk-operations.md](crud/bulk-operations.md) | Select multiple, bulk actions |
| [soft-delete.md](crud/soft-delete.md) | Archive, restore |
### Checkout (6)
| Template | Tests |
|---|---|
| [add-to-cart.md](checkout/add-to-cart.md) | Add item, update cart |
| [update-quantity.md](checkout/update-quantity.md) | Increase, decrease, remove |
| [apply-coupon.md](checkout/apply-coupon.md) | Valid/invalid/expired codes |
| [payment.md](checkout/payment.md) | Card form, validation, processing |
| [order-confirm.md](checkout/order-confirm.md) | Success page, order details |
| [order-history.md](checkout/order-history.md) | List orders, pagination |
### Search & Filter (5)
| Template | Tests |
|---|---|
| [basic-search.md](search/basic-search.md) | Search input, results |
| [filters.md](search/filters.md) | Category, price, checkboxes |
| [sorting.md](search/sorting.md) | Sort by name, date, price |
| [pagination.md](search/pagination.md) | Page nav, items per page |
| [empty-state.md](search/empty-state.md) | No results, clear filters |
### Forms (6)
| Template | Tests |
|---|---|
| [single-step.md](forms/single-step.md) | Simple form submission |
| [multi-step.md](forms/multi-step.md) | Wizard with progress |
| [validation.md](forms/validation.md) | Required, format, inline errors |
| [file-upload.md](forms/file-upload.md) | Single, multiple, drag-drop |
| [conditional-fields.md](forms/conditional-fields.md) | Show/hide based on selection |
| [autosave.md](forms/autosave.md) | Draft save, restore |
### Dashboard (5)
| Template | Tests |
|---|---|
| [data-loading.md](dashboard/data-loading.md) | Loading state, skeleton, data |
| [chart-rendering.md](dashboard/chart-rendering.md) | Chart visible, tooltips |
| [date-range-filter.md](dashboard/date-range-filter.md) | Date picker, presets |
| [export.md](dashboard/export.md) | CSV/PDF download |
| [realtime-updates.md](dashboard/realtime-updates.md) | Live data, websocket |
### Settings (4)
| Template | Tests |
|---|---|
| [profile-update.md](settings/profile-update.md) | Name, email, avatar |
| [password-change.md](settings/password-change.md) | Current + new password |
| [notification-prefs.md](settings/notification-prefs.md) | Toggle, save prefs |
| [account-delete.md](settings/account-delete.md) | Confirm deletion |
### Onboarding (4)
| Template | Tests |
|---|---|
| [registration.md](onboarding/registration.md) | Signup form, validation |
| [email-verification.md](onboarding/email-verification.md) | Verify link, resend |
| [welcome-tour.md](onboarding/welcome-tour.md) | Step tour, skip |
| [first-time-setup.md](onboarding/first-time-setup.md) | Initial config |
### Notifications (3)
| Template | Tests |
|---|---|
| [in-app.md](notifications/in-app.md) | Badge, dropdown, mark read |
| [toast-messages.md](notifications/toast-messages.md) | Success/error toasts |
| [notification-center.md](notifications/notification-center.md) | List, filter, clear |
### API Testing (5)
| Template | Tests |
|---|---|
| [rest-crud.md](api/rest-crud.md) | GET/POST/PUT/DELETE |
| [graphql.md](api/graphql.md) | Query, mutation |
| [auth-headers.md](api/auth-headers.md) | Token, expired, refresh |
| [error-responses.md](api/error-responses.md) | 400-500 status handling |
| [rate-limiting.md](api/rate-limiting.md) | Rate limit, retry-after |
### Accessibility (3)
| Template | Tests |
|---|---|
| [keyboard-navigation.md](accessibility/keyboard-navigation.md) | Tab order, focus |
| [screen-reader.md](accessibility/screen-reader.md) | ARIA labels, live regions |
| [color-contrast.md](accessibility/color-contrast.md) | Contrast ratios |
FILE:templates/accessibility/color-contrast.md
# Color Contrast Template
Tests contrast ratios, color-blind safe palettes, and focus indicator visibility.
## Prerequisites
- App running at `{{baseUrl}}`
- axe-playwright installed: `npm i -D @axe-core/playwright`
- Page under test: `{{baseUrl}}/{{pagePath}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Color Contrast', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{pagePath}}');
});
// Happy path: no color contrast violations (axe)
test('has no color contrast violations', async ({ page }) => {
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.withRules(['color-contrast'])
.analyze();
expect(results.violations).toEqual([]);
});
// Happy path: body text contrast ratio ≥ 4.5:1
test('body text meets WCAG AA contrast ratio', async ({ page }) => {
const ratio = await page.evaluate(() => {
const el = document.querySelector('p, main, [class*="body"]') as HTMLElement;
if (!el) return null;
const style = getComputedStyle(el);
// Simplified check — use axe for full verification
return style.color !== 'rgba(0, 0, 0, 0)' ? style.color : null;
});
expect(ratio).toBeTruthy();
});
// Happy path: large text contrast ratio ≥ 3:1
test('headings have sufficient contrast', async ({ page }) => {
const results = await new AxeBuilder({ page })
.withRules(['color-contrast'])
.include('h1, h2, h3, h4, h5, h6')
.analyze();
expect(results.violations).toEqual([]);
});
// Happy path: focus indicator meets contrast requirement
test('focus indicator is visible and meets contrast', async ({ page }) => {
await page.getByRole('button').first().focus();
const outline = await page.getByRole('button').first().evaluate(el => {
const s = getComputedStyle(el, ':focus');
return {
outlineWidth: parseFloat(s.outlineWidth),
outlineColor: s.outlineColor,
outlineStyle: s.outlineStyle,
};
});
expect(outline.outlineWidth).toBeGreaterThanOrEqual(2);
expect(outline.outlineColor).not.toBe('rgba(0, 0, 0, 0)');
});
// Happy path: error text contrast
test('error messages have sufficient contrast', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
await page.getByRole('button', { name: /submit/i }).click();
const results = await new AxeBuilder({ page })
.withRules(['color-contrast'])
.include('[class*="error"], [role="alert"]')
.analyze();
expect(results.violations).toEqual([]);
});
// Happy path: no information conveyed by color alone
test('status badges use text or icon in addition to color', async ({ page }) => {
const badges = page.getByRole('status');
const count = await badges.count();
for (let i = 0; i < count; i++) {
const text = await badges.nth(i).textContent();
const ariaLabel = await badges.nth(i).getAttribute('aria-label');
expect(text?.trim() || ariaLabel).toBeTruthy();
}
});
// Edge case: full page axe scan for all WCAG 2.1 AA issues
test('full page passes WCAG 2.1 AA axe scan', async ({ page }) => {
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.exclude('{{knownExcludedSelector}}')
.analyze();
if (results.violations.length > 0) {
const messages = results.violations.map(v =>
`v.id: v.description — v.nodes.map(n => n.target).join(', ')`
).join('\n');
throw new Error(`Axe violations:\nmessages`);
}
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;
test.describe('Color Contrast', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{pagePath}}');
});
test('no color contrast violations', async ({ page }) => {
const results = await new AxeBuilder({ page })
.withRules(['color-contrast'])
.analyze();
expect(results.violations).toEqual([]);
});
test('focus indicator is visible', async ({ page }) => {
await page.getByRole('button').first().focus();
const outlineWidth = await page.getByRole('button').first().evaluate(
el => parseFloat(getComputedStyle(el).outlineWidth)
);
expect(outlineWidth).toBeGreaterThanOrEqual(2);
});
test('status badges use text not just color', async ({ page }) => {
const badges = page.getByRole('status');
const count = await badges.count();
for (let i = 0; i < count; i++) {
const text = await badges.nth(i).textContent();
const label = await badges.nth(i).getAttribute('aria-label');
expect((text?.trim()) || label).toBeTruthy();
}
});
test('full page passes WCAG 2.1 AA', async ({ page }) => {
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze();
expect(results.violations).toEqual([]);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Contrast violations | axe color-contrast rule → no violations |
| Body text contrast | Text color non-transparent |
| Heading contrast | axe include h1-h6 → no violations |
| Focus indicator | outline-width ≥ 2px and non-transparent |
| Error text contrast | Error messages pass axe |
| Color-only info | Badges have text or aria-label |
| Full axe scan | WCAG 2.1 AA complete scan |
FILE:templates/accessibility/keyboard-navigation.md
# Keyboard Navigation Template
Tests tab order, focus visibility, and keyboard shortcuts.
## Prerequisites
- App running at `{{baseUrl}}`
- Page under test: `{{baseUrl}}/{{pagePath}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Keyboard Navigation', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{pagePath}}');
});
// Happy path: Tab moves through interactive elements in logical order
test('Tab key cycles through focusable elements in correct order', async ({ page }) => {
await page.keyboard.press('Tab');
await expect(page.getByRole('link', { name: /skip.*main|skip navigation/i }))
.toBeFocused();
await page.keyboard.press('Tab');
// First nav link focused
const navLinks = page.getByRole('navigation').getByRole('link');
await expect(navLinks.first()).toBeFocused();
});
// Happy path: skip link skips to main content
test('skip-to-content link moves focus to main', async ({ page }) => {
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
await expect(page.getByRole('main')).toBeFocused();
});
// Happy path: focus visible on all interactive elements
test('focus ring visible on interactive elements', async ({ page }) => {
const interactive = page.getByRole('button').first();
await interactive.focus();
const box = await interactive.boundingBox();
// Take screenshot with focus and assert element has outline (visual only — use CSS check)
const outline = await interactive.evaluate(el =>
getComputedStyle(el).outlineWidth
);
expect(parseFloat(outline)).toBeGreaterThan(0);
});
// Happy path: modal traps focus
test('focus is trapped within modal when open', async ({ page }) => {
await page.getByRole('button', { name: /open modal/i }).click();
const modal = page.getByRole('dialog');
await expect(modal).toBeVisible();
// Repeatedly Tab and verify focus stays within dialog
for (let i = 0; i < 10; i++) {
await page.keyboard.press('Tab');
const focused = page.locator(':focus');
await expect(modal).toContainElement(focused);
}
});
// Happy path: Escape closes modal
test('Escape key closes modal', async ({ page }) => {
await page.getByRole('button', { name: /open modal/i }).click();
await expect(page.getByRole('dialog')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).toBeHidden();
// Focus returns to trigger button
await expect(page.getByRole('button', { name: /open modal/i })).toBeFocused();
});
// Happy path: keyboard shortcut
test('keyboard shortcut {{shortcutKey}} triggers action', async ({ page }) => {
await page.keyboard.press('{{shortcutKey}}');
await expect(page.getByRole('{{shortcutTargetRole}}', { name: /{{shortcutTargetName}}/i })).toBeVisible();
});
// Error case: focus not lost on dynamic content update
test('focus stays on element after async update', async ({ page }) => {
const btn = page.getByRole('button', { name: /{{asyncButton}}/i });
await btn.focus();
await btn.press('Enter');
await expect(btn).toBeFocused();
});
// Edge case: arrow keys navigate within component (listbox, tabs)
test('arrow keys navigate within tab list', async ({ page }) => {
const firstTab = page.getByRole('tab').first();
await firstTab.focus();
await page.keyboard.press('ArrowRight');
await expect(page.getByRole('tab').nth(1)).toBeFocused();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Keyboard Navigation', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{pagePath}}');
});
test('skip link moves focus to main content', async ({ page }) => {
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
await expect(page.getByRole('main')).toBeFocused();
});
test('Escape closes modal and returns focus', async ({ page }) => {
await page.getByRole('button', { name: /open modal/i }).click();
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).toBeHidden();
await expect(page.getByRole('button', { name: /open modal/i })).toBeFocused();
});
test('focus ring visible on buttons', async ({ page }) => {
await page.getByRole('button').first().focus();
const outline = await page.getByRole('button').first().evaluate(
el => getComputedStyle(el).outlineWidth
);
expect(parseFloat(outline)).toBeGreaterThan(0);
});
test('arrow keys navigate tab list', async ({ page }) => {
await page.getByRole('tab').first().focus();
await page.keyboard.press('ArrowRight');
await expect(page.getByRole('tab').nth(1)).toBeFocused();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Tab order | Skip link first, nav links after |
| Skip link | Moves focus to `<main>` |
| Focus ring | CSS outline-width > 0 on focus |
| Focus trap | Tab stays within open modal |
| Escape closes | Modal closed, trigger re-focused |
| Keyboard shortcut | Custom key triggers action |
| Focus after update | Focus not lost on async update |
| Arrow keys | Tab/listbox/menu arrow navigation |
FILE:templates/accessibility/screen-reader.md
# Screen Reader Template
Tests ARIA labels, live regions, and announcements for assistive technology.
## Prerequisites
- App running at `{{baseUrl}}`
- Page under test: `{{baseUrl}}/{{pagePath}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Screen Reader Accessibility', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{pagePath}}');
});
// Happy path: page has descriptive title
test('page has meaningful title', async ({ page }) => {
await expect(page).toHaveTitle(/{{expectedPageTitle}}/i);
});
// Happy path: main landmark exists
test('page has main landmark', async ({ page }) => {
await expect(page.getByRole('main')).toBeVisible();
});
// Happy path: images have alt text
test('informational images have non-empty alt text', async ({ page }) => {
const images = page.getByRole('img');
const count = await images.count();
for (let i = 0; i < count; i++) {
const alt = await images.nth(i).getAttribute('alt');
const isDecorative = await images.nth(i).getAttribute('role') === 'presentation'
|| alt === '';
if (!isDecorative) {
expect(alt).toBeTruthy();
}
}
});
// Happy path: form fields have accessible labels
test('all form inputs have associated labels', async ({ page }) => {
const inputs = page.getByRole('textbox');
const count = await inputs.count();
for (let i = 0; i < count; i++) {
const input = inputs.nth(i);
const labelledBy = await input.getAttribute('aria-labelledby');
const ariaLabel = await input.getAttribute('aria-label');
const id = await input.getAttribute('id');
const hasLabel = labelledBy || ariaLabel || (id && await page.locator(`label[for="id"]`).count() > 0);
expect(hasLabel).toBeTruthy();
}
});
// Happy path: live region announces updates
test('live region announces async updates', async ({ page }) => {
const liveRegion = page.getByRole('status').or(page.locator('[aria-live]'));
await page.getByRole('button', { name: /{{asyncTrigger}}/i }).click();
await expect(liveRegion).not.toBeEmpty();
});
// Happy path: alert role used for errors
test('validation errors use role="alert"', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
await page.getByRole('button', { name: /submit/i }).click();
await expect(page.getByRole('alert')).toBeVisible();
const liveValue = await page.getByRole('alert').first().getAttribute('aria-live');
expect(liveValue ?? 'assertive').toBe('assertive');
});
// Happy path: buttons have accessible names
test('icon-only buttons have aria-label', async ({ page }) => {
const buttons = page.getByRole('button');
const count = await buttons.count();
for (let i = 0; i < count; i++) {
const btn = buttons.nth(i);
const text = (await btn.textContent())?.trim();
const ariaLabel = await btn.getAttribute('aria-label');
const ariaLabelledBy = await btn.getAttribute('aria-labelledby');
// Must have visible text or aria-label or aria-labelledby
expect(text || ariaLabel || ariaLabelledBy).toBeTruthy();
}
});
// Happy path: navigation landmark labelled
test('multiple nav elements have distinct aria-labels', async ({ page }) => {
const navs = page.getByRole('navigation');
const count = await navs.count();
if (count > 1) {
const labels = new Set<string>();
for (let i = 0; i < count; i++) {
const label = await navs.nth(i).getAttribute('aria-label') ?? '';
labels.add(label);
}
expect(labels.size).toBe(count); // all unique
}
});
// Edge case: expanded/collapsed state communicated
test('accordion aria-expanded reflects open/closed state', async ({ page }) => {
const trigger = page.getByRole('button', { name: /{{accordionItem}}/i });
await expect(trigger).toHaveAttribute('aria-expanded', 'false');
await trigger.click();
await expect(trigger).toHaveAttribute('aria-expanded', 'true');
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Screen Reader Accessibility', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{pagePath}}');
});
test('page has meaningful title', async ({ page }) => {
await expect(page).toHaveTitle(/{{expectedPageTitle}}/i);
});
test('main landmark exists', async ({ page }) => {
await expect(page.getByRole('main')).toBeVisible();
});
test('validation errors use role=alert', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
await page.getByRole('button', { name: /submit/i }).click();
await expect(page.getByRole('alert')).toBeVisible();
});
test('accordion aria-expanded toggles', async ({ page }) => {
const trigger = page.getByRole('button', { name: /{{accordionItem}}/i });
await expect(trigger).toHaveAttribute('aria-expanded', 'false');
await trigger.click();
await expect(trigger).toHaveAttribute('aria-expanded', 'true');
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Page title | `<title>` matches expected pattern |
| Main landmark | `<main>` present and visible |
| Image alt text | Informational images have non-empty alt |
| Form labels | All inputs have accessible label |
| Live region | Status region updated on async action |
| Alert role | Errors use role=alert (assertive) |
| Button names | Icon buttons have aria-label |
| Unique nav labels | Multiple navs have distinct labels |
| aria-expanded | Accordion state communicated |
FILE:templates/api/auth-headers.md
# Auth Headers Template
Tests token authentication, expired token handling, and token refresh flow.
## Prerequisites
- Valid token: `{{apiToken}}`
- Expired token: `{{expiredApiToken}}`
- Refresh token: `{{refreshToken}}`
- API base: `{{apiBaseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('API Auth Headers', () => {
// Happy path: valid Bearer token accepted
test('accepts valid Bearer token', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/me', {
headers: { 'Authorization': `Bearer {{apiToken}}` },
});
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.id).toBeTruthy();
});
// Happy path: API key in header accepted
test('accepts API key header', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/{{entityName}}s', {
headers: { 'X-API-Key': '{{apiKey}}' },
});
expect(res.status()).toBe(200);
});
// Error case: no auth header returns 401
test('returns 401 without auth header', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/me');
expect(res.status()).toBe(401);
const body = await res.json();
expect(body.error ?? body.message).toMatch(/unauthorized|authentication required/i);
});
// Error case: expired token returns 401
test('returns 401 for expired token', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/me', {
headers: { 'Authorization': `Bearer {{expiredApiToken}}` },
});
expect(res.status()).toBe(401);
const body = await res.json();
expect(body.error ?? body.code).toMatch(/token.*expired|expired_token/i);
});
// Happy path: refresh token obtains new access token
test('refreshes expired token and retries request', async ({ request }) => {
// Step 1: refresh
const refresh = await request.post('{{apiBaseUrl}}/auth/refresh', {
data: { refresh_token: '{{refreshToken}}' },
});
expect(refresh.status()).toBe(200);
const { access_token } = await refresh.json();
expect(access_token).toBeTruthy();
// Step 2: use new token
const res = await request.get('{{apiBaseUrl}}/me', {
headers: { 'Authorization': `Bearer access_token` },
});
expect(res.status()).toBe(200);
});
// Error case: invalid token format returns 401
test('returns 401 for malformed token', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/me', {
headers: { 'Authorization': 'Bearer not.a.jwt' },
});
expect(res.status()).toBe(401);
});
// Edge case: token in cookie vs header
test('accepts session cookie as auth alternative', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/me', {
headers: { 'Cookie': `{{sessionCookieName}}={{sessionCookieValue}}` },
});
expect(res.status()).toBe(200);
});
// Edge case: revoked token returns 401
test('returns 401 for revoked token', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/me', {
headers: { 'Authorization': `Bearer {{revokedApiToken}}` },
});
expect(res.status()).toBe(401);
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('API Auth Headers', () => {
test('accepts valid Bearer token', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/me', {
headers: { 'Authorization': `Bearer {{apiToken}}` },
});
expect(res.status()).toBe(200);
});
test('returns 401 without auth header', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/me');
expect(res.status()).toBe(401);
});
test('returns 401 for expired token', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/me', {
headers: { 'Authorization': `Bearer {{expiredApiToken}}` },
});
expect(res.status()).toBe(401);
});
test('refreshes token and retries', async ({ request }) => {
const refresh = await request.post('{{apiBaseUrl}}/auth/refresh', {
data: { refresh_token: '{{refreshToken}}' },
});
const { access_token } = await refresh.json();
const res = await request.get('{{apiBaseUrl}}/me', {
headers: { 'Authorization': `Bearer access_token` },
});
expect(res.status()).toBe(200);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Valid Bearer | 200 with user data |
| API key | X-API-Key header accepted |
| No auth | 401 + error message |
| Expired token | 401 + expired error code |
| Token refresh | New token from refresh endpoint |
| Malformed token | 401 for non-JWT |
| Cookie auth | Session cookie accepted |
| Revoked token | 401 for revoked token |
FILE:templates/api/error-responses.md
# API Error Responses Template
Tests 400, 401, 403, 404, and 500 HTTP error handling.
## Prerequisites
- Valid auth token: `{{apiToken}}`
- API base: `{{apiBaseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
const validHeaders = {
'Authorization': `Bearer {{apiToken}}`,
'Content-Type': 'application/json',
};
test.describe('API Error Responses', () => {
// 400 Bad Request
test('POST with invalid body returns 400', async ({ request }) => {
const res = await request.post('{{apiBaseUrl}}/{{entityName}}s', {
headers: validHeaders,
data: { name: '' }, // name too short / blank
});
expect(res.status()).toBe(400);
const body = await res.json();
expect(body.message ?? body.error).toMatch(/bad request|invalid/i);
expect(body.errors ?? body.details).toBeDefined();
});
// 401 Unauthorized
test('request without token returns 401', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/{{entityName}}s');
expect(res.status()).toBe(401);
const body = await res.json();
expect(body.message ?? body.error).toMatch(/unauthorized|authentication/i);
});
// 403 Forbidden
test('accessing admin endpoint as regular user returns 403', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/admin/users', {
headers: { 'Authorization': `Bearer {{userToken}}` },
});
expect(res.status()).toBe(403);
const body = await res.json();
expect(body.message ?? body.error).toMatch(/forbidden|insufficient.*permission/i);
});
// 404 Not Found
test('GET non-existent resource returns 404', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/{{entityName}}s/999999', { headers: validHeaders });
expect(res.status()).toBe(404);
const body = await res.json();
expect(body.message ?? body.error).toMatch(/not found/i);
});
// 422 Unprocessable Entity
test('POST with missing required field returns 422', async ({ request }) => {
const res = await request.post('{{apiBaseUrl}}/{{entityName}}s', {
headers: validHeaders,
data: { description: 'no name provided' },
});
expect([422, 400]).toContain(res.status());
const body = await res.json();
expect(body.errors ?? body.details).toBeDefined();
});
// 429 Too Many Requests (handled in rate-limiting template — kept here for completeness)
test('returns 429 when rate limit exceeded', async ({ request }) => {
let lastStatus = 0;
for (let i = 0; i < {{rateLimitThreshold}} + 1; i++) {
const res = await request.get('{{apiBaseUrl}}/{{rateLimitedEndpoint}}', { headers: validHeaders });
lastStatus = res.status();
if (lastStatus === 429) break;
}
expect(lastStatus).toBe(429);
});
// 500 Internal Server Error
test('server error returns 500 with error body', async ({ page }) => {
await page.route('{{apiBaseUrl}}/{{entityName}}s', route =>
route.fulfill({ status: 500, body: JSON.stringify({ error: 'Internal Server Error' }) })
);
const res = await page.request.get('{{apiBaseUrl}}/{{entityName}}s', { headers: validHeaders });
expect(res.status()).toBe(500);
const body = await res.json();
expect(body.error ?? body.message).toBeTruthy();
});
// Edge case: error response has consistent shape
test('all errors return JSON with error field', async ({ request }) => {
const endpoints = [
{ method: 'get' as const, url: '{{apiBaseUrl}}/{{entityName}}s/000000', headers: validHeaders },
{ method: 'get' as const, url: '{{apiBaseUrl}}/{{entityName}}s' },
];
for (const ep of endpoints) {
const res = await request[ep.method](ep.url, { headers: ep.headers });
if (res.status() >= 400) {
const body = await res.json();
expect(body.error ?? body.message ?? body.errors).toBeDefined();
}
}
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
const headers = { 'Authorization': `Bearer {{apiToken}}`, 'Content-Type': 'application/json' };
test.describe('API Error Responses', () => {
test('POST with invalid body returns 400', async ({ request }) => {
const res = await request.post('{{apiBaseUrl}}/{{entityName}}s', {
headers,
data: { name: '' },
});
expect(res.status()).toBe(400);
});
test('no token returns 401', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/{{entityName}}s');
expect(res.status()).toBe(401);
});
test('regular user on admin endpoint returns 403', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/admin/users', {
headers: { 'Authorization': `Bearer {{userToken}}` },
});
expect(res.status()).toBe(403);
});
test('non-existent resource returns 404', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/{{entityName}}s/999999', { headers });
expect(res.status()).toBe(404);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| 400 Bad Request | Invalid body → 400 + errors detail |
| 401 Unauthorized | No token → 401 |
| 403 Forbidden | Wrong role → 403 |
| 404 Not Found | Missing resource → 404 |
| 422 Unprocessable | Missing required field → 422/400 |
| 429 Rate Limit | Threshold exceeded → 429 |
| 500 Server Error | Mocked 500 → error body present |
| Consistent shape | All errors have error/message field |
FILE:templates/api/graphql.md
# GraphQL API Template
Tests query, mutation, and subscription via Playwright's request API.
## Prerequisites
- Valid auth token: `{{apiToken}}`
- GraphQL endpoint: `{{graphqlEndpoint}}`
- WebSocket endpoint for subscriptions: `{{graphqlWsEndpoint}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
const GQL_URL = '{{graphqlEndpoint}}';
const headers = {
'Authorization': `Bearer {{apiToken}}`,
'Content-Type': 'application/json',
};
async function gql(request: any, query: string, variables = {}) {
const res = await request.post(GQL_URL, { headers, data: { query, variables } });
const body = await res.json();
expect(body.errors).toBeUndefined();
return body.data;
}
test.describe('GraphQL API', () => {
// Happy path: query
test('query fetches {{entityName}} list', async ({ request }) => {
const data = await gql(request, `
query Get{{EntityName}}s($limit: Int) {
{{entityName}}s(limit: $limit) { id name createdAt }
}
`, { limit: 10 });
expect(Array.isArray(data.{{entityName}}s)).toBe(true);
expect(data.{{entityName}}s.length).toBeLessThanOrEqual(10);
});
// Happy path: query single entity
test('query fetches single {{entityName}} by id', async ({ request }) => {
const data = await gql(request, `
query Get{{EntityName}}($id: ID!) {
{{entityName}}(id: $id) { id name description }
}
`, { id: '{{existingEntityId}}' });
expect(data.{{entityName}}.id).toBe('{{existingEntityId}}');
});
// Happy path: mutation creates entity
test('mutation creates {{entityName}}', async ({ request }) => {
const data = await gql(request, `
mutation Create{{EntityName}}($input: {{EntityName}}Input!) {
create{{EntityName}}(input: $input) { id name }
}
`, { input: { name: '{{testEntityName}}', description: '{{testDescription}}' } });
expect(data.create{{EntityName}}.id).toBeTruthy();
expect(data.create{{EntityName}}.name).toBe('{{testEntityName}}');
});
// Happy path: mutation updates entity
test('mutation updates {{entityName}}', async ({ request }) => {
const data = await gql(request, `
mutation Update{{EntityName}}($id: ID!, $input: {{EntityName}}Input!) {
update{{EntityName}}(id: $id, input: $input) { id name }
}
`, { id: '{{existingEntityId}}', input: { name: '{{updatedName}}' } });
expect(data.update{{EntityName}}.name).toBe('{{updatedName}}');
});
// Happy path: mutation deletes entity
test('mutation deletes {{entityName}}', async ({ request }) => {
const data = await gql(request, `
mutation Delete{{EntityName}}($id: ID!) {
delete{{EntityName}}(id: $id) { success }
}
`, { id: '{{deletableEntityId}}' });
expect(data.delete{{EntityName}}.success).toBe(true);
});
// Error case: invalid query returns errors array
test('invalid query returns errors', async ({ request }) => {
const res = await request.post(GQL_URL, {
headers,
data: { query: '{ invalidField }' },
});
const body = await res.json();
expect(body.errors).toBeDefined();
expect(body.errors.length).toBeGreaterThan(0);
});
// Error case: unauthorized query
test('query without auth returns unauthorized error', async ({ request }) => {
const res = await request.post(GQL_URL, {
headers: { 'Content-Type': 'application/json' }, // No auth
data: { query: '{ {{entityName}}s { id } }' },
});
const body = await res.json();
expect(body.errors?.[0]?.extensions?.code).toMatch(/UNAUTHENTICATED|UNAUTHORIZED/);
});
// Edge case: subscription via page WebSocket
test('subscription receives real-time update', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
const received: any[] = [];
await page.evaluate(() => {
const ws = new WebSocket('{{graphqlWsEndpoint}}');
ws.onmessage = e => (window as any).__gqlMsg = JSON.parse(e.data);
});
// Trigger mutation to fire subscription
await page.request.post(GQL_URL, {
headers,
data: { query: 'mutation { trigger{{EntityName}}Event { id } }' },
});
const msg = await page.evaluate(() => (window as any).__gqlMsg);
expect(msg?.type).toBe('data');
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
const headers = { 'Authorization': `Bearer {{apiToken}}`, 'Content-Type': 'application/json' };
async function gql(request, query, variables = {}) {
const res = await request.post('{{graphqlEndpoint}}', { headers, data: { query, variables } });
const body = await res.json();
expect(body.errors).toBeUndefined();
return body.data;
}
test.describe('GraphQL API', () => {
test('query fetches entity list', async ({ request }) => {
const data = await gql(request, '{ {{entityName}}s { id name } }');
expect(Array.isArray(data.{{entityName}}s)).toBe(true);
});
test('mutation creates entity', async ({ request }) => {
const data = await gql(request,
'mutation($input: {{EntityName}}Input!) { create{{EntityName}}(input: $input) { id } }',
{ input: { name: '{{testEntityName}}' } }
);
expect(data.create{{EntityName}}.id).toBeTruthy();
});
test('invalid query returns errors array', async ({ request }) => {
const res = await request.post('{{graphqlEndpoint}}', {
headers,
data: { query: '{ nonExistentField }' },
});
const body = await res.json();
expect(body.errors?.length).toBeGreaterThan(0);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| List query | Returns array of entities |
| Single query | Returns entity by ID |
| Create mutation | Returns new entity with ID |
| Update mutation | Returns updated field value |
| Delete mutation | Returns success: true |
| Invalid query | errors[] defined in response |
| Unauthenticated | UNAUTHENTICATED extension code |
| Subscription | Real-time message via WebSocket |
FILE:templates/api/rate-limiting.md
# Rate Limiting Template
Tests rate limit headers, 429 response, and Retry-After handling.
## Prerequisites
- Valid auth token: `{{apiToken}}`
- Rate-limited endpoint: `{{rateLimitedEndpoint}}`
- Rate limit: `{{rateLimit}}` requests per `{{rateLimitWindow}}`
- API base: `{{apiBaseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
const headers = {
'Authorization': `Bearer {{apiToken}}`,
'Content-Type': 'application/json',
};
test.describe('Rate Limiting', () => {
// Happy path: rate limit headers present on normal requests
test('includes rate limit headers on success response', async ({ request }) => {
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
expect(res.status()).toBe(200);
expect(res.headers()['x-ratelimit-limit']).toBeTruthy();
expect(res.headers()['x-ratelimit-remaining']).toBeTruthy();
expect(Number(res.headers()['x-ratelimit-limit'])).toBe({{rateLimit}});
});
// Happy path: remaining count decrements
test('x-ratelimit-remaining decrements with each request', async ({ request }) => {
const first = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
const second = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
const remaining1 = Number(first.headers()['x-ratelimit-remaining']);
const remaining2 = Number(second.headers()['x-ratelimit-remaining']);
expect(remaining2).toBeLessThan(remaining1);
});
// Error case: 429 when limit exceeded
test('returns 429 when rate limit exceeded', async ({ request }) => {
let lastStatus = 200;
let retryAfter: string | undefined;
for (let i = 0; i <= {{rateLimit}}; i++) {
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
lastStatus = res.status();
if (lastStatus === 429) {
retryAfter = res.headers()['retry-after'];
break;
}
}
expect(lastStatus).toBe(429);
expect(retryAfter).toBeTruthy();
});
// Error case: 429 body contains error message
test('429 response body contains error and retry info', async ({ request }) => {
// Exhaust limit
for (let i = 0; i <= {{rateLimit}}; i++) {
await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
}
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
if (res.status() === 429) {
const body = await res.json();
expect(body.error ?? body.message).toMatch(/rate limit|too many requests/i);
expect(Number(res.headers()['retry-after'])).toBeGreaterThan(0);
}
});
// Happy path: different users have separate rate limit buckets
test('rate limit is per-user, not global', async ({ request }) => {
// Exhaust limit for user 1
for (let i = 0; i <= {{rateLimit}}; i++) {
await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, {
headers: { 'Authorization': `Bearer {{apiToken}}` },
});
}
// User 2 should still succeed
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, {
headers: { 'Authorization': `Bearer {{apiToken2}}` },
});
expect(res.status()).toBe(200);
});
// Edge case: reset after window expires
test('rate limit resets after window expires', async ({ page, request }) => {
// Exhaust limit
for (let i = 0; i <= {{rateLimit}}; i++) {
await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
}
// Advance clock past the window
await page.clock.install();
await page.clock.fastForward({{rateLimitWindowMs}});
// Should succeed again
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
expect(res.status()).toBe(200);
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
const headers = { 'Authorization': `Bearer {{apiToken}}` };
test.describe('Rate Limiting', () => {
test('includes rate limit headers on success', async ({ request }) => {
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
expect(res.status()).toBe(200);
expect(res.headers()['x-ratelimit-limit']).toBeTruthy();
expect(res.headers()['x-ratelimit-remaining']).toBeTruthy();
});
test('returns 429 with Retry-After when limit exceeded', async ({ request }) => {
let lastStatus = 200;
let retryAfter;
for (let i = 0; i <= {{rateLimit}}; i++) {
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
lastStatus = res.status();
if (lastStatus === 429) { retryAfter = res.headers()['retry-after']; break; }
}
expect(lastStatus).toBe(429);
expect(retryAfter).toBeTruthy();
});
test('per-user buckets: other user unaffected', async ({ request }) => {
for (let i = 0; i <= {{rateLimit}}; i++) {
await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
}
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, {
headers: { 'Authorization': `Bearer {{apiToken2}}` },
});
expect(res.status()).toBe(200);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Headers present | x-ratelimit-limit and -remaining on 200 |
| Decrement | remaining decreases each request |
| 429 triggered | Limit exceeded → 429 + Retry-After |
| 429 body | Error message + retry info in body |
| Per-user bucket | Exhausted user doesn't affect others |
| Window reset | Clock advanced → limit resets |
FILE:templates/api/rest-crud.md
# REST CRUD API Template
Tests GET, POST, PUT, and DELETE API endpoints directly via Playwright's request API.
## Prerequisites
- Valid auth token: `{{apiToken}}`
- Base API URL: `{{apiBaseUrl}}`
- Test entity endpoint: `/{{entityName}}s`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('REST CRUD — /{{entityName}}s', () => {
let createdId: string;
const headers = {
'Authorization': `Bearer {{apiToken}}`,
'Content-Type': 'application/json',
};
// Happy path: GET list
test('GET /{{entityName}}s returns list', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/{{entityName}}s', { headers });
expect(res.status()).toBe(200);
const body = await res.json();
expect(Array.isArray(body.data ?? body)).toBe(true);
});
// Happy path: POST creates entity
test('POST /{{entityName}}s creates new entity', async ({ request }) => {
const res = await request.post('{{apiBaseUrl}}/{{entityName}}s', {
headers,
data: { name: '{{testEntityName}}', description: '{{testDescription}}' },
});
expect(res.status()).toBe(201);
const body = await res.json();
expect(body.id).toBeTruthy();
expect(body.name).toBe('{{testEntityName}}');
createdId = body.id;
});
// Happy path: GET single entity
test('GET /{{entityName}}s/:id returns entity', async ({ request }) => {
const res = await request.get(`{{apiBaseUrl}}/{{entityName}}s/{{existingEntityId}}`, { headers });
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.id).toBe('{{existingEntityId}}');
expect(body.name).toBeTruthy();
});
// Happy path: PUT updates entity
test('PUT /{{entityName}}s/:id updates entity', async ({ request }) => {
const res = await request.put(`{{apiBaseUrl}}/{{entityName}}s/{{existingEntityId}}`, {
headers,
data: { name: '{{updatedEntityName}}' },
});
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.name).toBe('{{updatedEntityName}}');
});
// Happy path: PATCH partial update
test('PATCH /{{entityName}}s/:id partially updates entity', async ({ request }) => {
const res = await request.patch(`{{apiBaseUrl}}/{{entityName}}s/{{existingEntityId}}`, {
headers,
data: { description: '{{patchedDescription}}' },
});
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.description).toBe('{{patchedDescription}}');
});
// Happy path: DELETE removes entity
test('DELETE /{{entityName}}s/:id deletes entity', async ({ request }) => {
const del = await request.delete(`{{apiBaseUrl}}/{{entityName}}s/{{deletableEntityId}}`, { headers });
expect(del.status()).toBe(204);
// Verify gone
const get = await request.get(`{{apiBaseUrl}}/{{entityName}}s/{{deletableEntityId}}`, { headers });
expect(get.status()).toBe(404);
});
// Error case: POST with missing required field returns 422
test('POST with missing required field returns 422', async ({ request }) => {
const res = await request.post('{{apiBaseUrl}}/{{entityName}}s', {
headers,
data: {},
});
expect(res.status()).toBe(422);
const body = await res.json();
expect(body.errors).toBeTruthy();
});
// Error case: GET non-existent entity returns 404
test('GET non-existent entity returns 404', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/{{entityName}}s/999999', { headers });
expect(res.status()).toBe(404);
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
const headers = {
'Authorization': `Bearer {{apiToken}}`,
'Content-Type': 'application/json',
};
test.describe('REST CRUD — /{{entityName}}s', () => {
test('GET list returns 200 and array', async ({ request }) => {
const res = await request.get('{{apiBaseUrl}}/{{entityName}}s', { headers });
expect(res.status()).toBe(200);
const body = await res.json();
expect(Array.isArray(body.data ?? body)).toBe(true);
});
test('POST creates entity and returns 201', async ({ request }) => {
const res = await request.post('{{apiBaseUrl}}/{{entityName}}s', {
headers,
data: { name: '{{testEntityName}}' },
});
expect(res.status()).toBe(201);
expect((await res.json()).id).toBeTruthy();
});
test('DELETE removes entity, GET returns 404', async ({ request }) => {
await request.delete(`{{apiBaseUrl}}/{{entityName}}s/{{deletableEntityId}}`, { headers });
const res = await request.get(`{{apiBaseUrl}}/{{entityName}}s/{{deletableEntityId}}`, { headers });
expect(res.status()).toBe(404);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| GET list | 200 + array body |
| POST create | 201 + id in response |
| GET single | 200 + correct entity body |
| PUT update | 200 + updated field in response |
| PATCH partial | 200 + patched field only changed |
| DELETE | 204 → subsequent GET returns 404 |
| POST validation | Missing field → 422 + errors |
| GET 404 | Non-existent ID → 404 |
FILE:templates/auth/login.md
# Login Template
Tests email/password login, social login, and remember me functionality.
## Prerequisites
- Valid user account: `{{username}}` / `{{password}}`
- Social provider configured (Google/GitHub)
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Login', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/login');
});
// Happy path: email/password login
test('logs in with valid credentials', async ({ page }) => {
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{password}}');
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
});
// Happy path: remember me
test('persists session with remember me checked', async ({ page, context }) => {
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{password}}');
await page.getByRole('checkbox', { name: /remember me/i }).check();
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
const cookies = await context.cookies();
const session = cookies.find(c => c.name === '{{sessionCookieName}}');
expect(session?.expires).toBeGreaterThan(Date.now() / 1000 + 86400);
});
// Happy path: social login
test('redirects to social provider', async ({ page }) => {
await page.getByRole('button', { name: /continue with google/i }).click();
await expect(page).toHaveURL(/accounts\.google\.com/);
});
// Error case: invalid credentials
test('shows error for wrong password', async ({ page }) => {
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('wrong-password');
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page.getByRole('alert')).toContainText(/invalid.*credentials/i);
await expect(page).toHaveURL('{{baseUrl}}/login');
});
// Edge case: empty fields
test('shows validation for empty submission', async ({ page }) => {
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page.getByRole('textbox', { name: /email/i })).toBeFocused();
await expect(page.getByText(/email is required/i)).toBeVisible();
});
// Edge case: locked account
test('shows account locked message after multiple failures', async ({ page }) => {
for (let i = 0; i < {{lockoutAttempts}}; i++) {
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('wrong');
await page.getByRole('button', { name: /sign in/i }).click();
}
await expect(page.getByRole('alert')).toContainText(/account.*locked/i);
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Login', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/login');
});
test('logs in with valid credentials', async ({ page }) => {
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{password}}');
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
});
test('shows error for wrong password', async ({ page }) => {
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('wrong-password');
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page.getByRole('alert')).toContainText(/invalid.*credentials/i);
});
test('shows validation for empty submission', async ({ page }) => {
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page.getByText(/email is required/i)).toBeVisible();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Happy path | Valid credentials → dashboard redirect |
| Remember me | Long-lived cookie set |
| Social login | OAuth redirect to provider |
| Wrong password | Alert with error message |
| Empty form | Inline validation shown |
| Locked account | Lockout message after N failures |
FILE:templates/auth/logout.md
# Logout Template
Tests logout from navigation, session cleanup, and redirect behaviour.
## Prerequisites
- Authenticated session (use `storageState` or login fixture)
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Logout', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
// Happy path: logout via nav menu
test('logs out from user menu', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /user menu/i }).click();
await page.getByRole('menuitem', { name: /sign out/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/login');
await expect(page.getByRole('heading', { name: /sign in/i })).toBeVisible();
});
// Happy path: session cookies cleared
test('clears session cookie on logout', async ({ page, context }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /user menu/i }).click();
await page.getByRole('menuitem', { name: /sign out/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/login');
const cookies = await context.cookies();
const session = cookies.find(c => c.name === '{{sessionCookieName}}');
expect(session).toBeUndefined();
});
// Happy path: accessing protected page after logout redirects
test('redirects to login when accessing protected page after logout', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /user menu/i }).click();
await page.getByRole('menuitem', { name: /sign out/i }).click();
await page.goto('{{baseUrl}}/dashboard');
await expect(page).toHaveURL(/\/login/);
});
// Error case: double logout (stale session)
test('handles logout gracefully when session already expired', async ({ page, context }) => {
await page.goto('{{baseUrl}}/dashboard');
await context.clearCookies();
await page.getByRole('button', { name: /user menu/i }).click();
await page.getByRole('menuitem', { name: /sign out/i }).click();
await expect(page).toHaveURL(/\/login/);
});
// Edge case: logout from multiple tabs
test('invalidates session across tabs', async ({ page, context }) => {
const tab2 = await context.newPage();
await page.goto('{{baseUrl}}/dashboard');
await tab2.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /user menu/i }).click();
await page.getByRole('menuitem', { name: /sign out/i }).click();
await tab2.reload();
await expect(tab2).toHaveURL(/\/login/);
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Logout', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('logs out from user menu', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /user menu/i }).click();
await page.getByRole('menuitem', { name: /sign out/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/login');
});
test('clears session cookie on logout', async ({ page, context }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /user menu/i }).click();
await page.getByRole('menuitem', { name: /sign out/i }).click();
const cookies = await context.cookies();
expect(cookies.find(c => c.name === '{{sessionCookieName}}')).toBeUndefined();
});
test('redirects protected page to login after logout', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /user menu/i }).click();
await page.getByRole('menuitem', { name: /sign out/i }).click();
await page.goto('{{baseUrl}}/dashboard');
await expect(page).toHaveURL(/\/login/);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Happy path | Nav menu → sign out → login page |
| Cookie cleanup | Session cookie removed after logout |
| Protected redirect | Accessing /dashboard after logout → /login |
| Stale session | Already-expired session handled gracefully |
| Multi-tab | Logout invalidates other open tabs |
FILE:templates/auth/mfa.md
# MFA Template
Tests 2FA TOTP code entry, backup codes, and MFA enrollment flow.
## Prerequisites
- MFA-enabled account: `{{mfaUsername}}` / `{{mfaPassword}}`
- TOTP secret for generating codes: `{{totpSecret}}`
- Backup code: `{{backupCode}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
import { authenticator } from 'otplib'; // npm i otplib
test.describe('MFA', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('textbox', { name: /email/i }).fill('{{mfaUsername}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{mfaPassword}}');
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page).toHaveURL(/\/mfa|\/two-factor/);
});
// Happy path: valid TOTP code
test('accepts valid TOTP code', async ({ page }) => {
const token = authenticator.generate('{{totpSecret}}');
await page.getByRole('textbox', { name: /code|token/i }).fill(token);
await page.getByRole('button', { name: /verify/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
});
// Happy path: backup code
test('accepts backup code', async ({ page }) => {
await page.getByRole('link', { name: /use backup code/i }).click();
await page.getByRole('textbox', { name: /backup code/i }).fill('{{backupCode}}');
await page.getByRole('button', { name: /verify/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
// Backup code consumed — warning shown
await expect(page.getByRole('alert')).toContainText(/backup code used/i);
});
// Error case: wrong TOTP code
test('rejects invalid TOTP code', async ({ page }) => {
await page.getByRole('textbox', { name: /code|token/i }).fill('000000');
await page.getByRole('button', { name: /verify/i }).click();
await expect(page.getByRole('alert')).toContainText(/invalid.*code/i);
await expect(page).toHaveURL(/\/mfa|\/two-factor/);
});
// Error case: expired code (simulate by providing code + 1 step)
test('rejects expired TOTP code', async ({ page }) => {
const expiredToken = authenticator.generate('{{totpSecret}}');
// Advance time simulation via clock if supported, else use a fixed stale code
await page.getByRole('textbox', { name: /code|token/i }).fill(expiredToken);
await page.clock.fastForward(60_000); // advance 60s past TOTP window
await page.getByRole('button', { name: /verify/i }).click();
await expect(page.getByRole('alert')).toContainText(/expired|invalid.*code/i);
});
// Edge case: MFA enrollment for new user
test('enrolls MFA via QR code scan', async ({ page: enrollPage }) => {
await enrollPage.goto('{{baseUrl}}/settings/security');
await enrollPage.getByRole('button', { name: /enable.*two-factor/i }).click();
await expect(enrollPage.getByRole('img', { name: /qr code/i })).toBeVisible();
await expect(enrollPage.getByText(/scan.*authenticator/i)).toBeVisible();
// User scans QR → enters token
const token = authenticator.generate('{{totpSecret}}');
await enrollPage.getByRole('textbox', { name: /verification code/i }).fill(token);
await enrollPage.getByRole('button', { name: /activate/i }).click();
await expect(enrollPage.getByRole('heading', { name: /backup codes/i })).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
const { authenticator } = require('otplib');
test.describe('MFA', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('textbox', { name: /email/i }).fill('{{mfaUsername}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{mfaPassword}}');
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page).toHaveURL(/\/mfa|\/two-factor/);
});
test('accepts valid TOTP code', async ({ page }) => {
const token = authenticator.generate('{{totpSecret}}');
await page.getByRole('textbox', { name: /code|token/i }).fill(token);
await page.getByRole('button', { name: /verify/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
});
test('accepts backup code', async ({ page }) => {
await page.getByRole('link', { name: /use backup code/i }).click();
await page.getByRole('textbox', { name: /backup code/i }).fill('{{backupCode}}');
await page.getByRole('button', { name: /verify/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
});
test('rejects invalid TOTP code', async ({ page }) => {
await page.getByRole('textbox', { name: /code|token/i }).fill('000000');
await page.getByRole('button', { name: /verify/i }).click();
await expect(page.getByRole('alert')).toContainText(/invalid.*code/i);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Valid TOTP | Correct time-based code → dashboard |
| Backup code | Single-use backup code accepted; warning shown |
| Invalid code | Wrong code → alert, stays on MFA page |
| Expired code | Clock-advanced token rejected |
| MFA enrollment | QR shown → token verified → backup codes displayed |
FILE:templates/auth/password-reset.md
# Password Reset Template
Tests reset request, setting a new password, and expired link handling.
## Prerequisites
- Account with email: `{{username}}`
- Reset link / token available in test environment (`{{resetToken}}`)
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Password Reset', () => {
// Happy path: request reset email
test('sends reset email for known address', async ({ page }) => {
await page.goto('{{baseUrl}}/forgot-password');
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('button', { name: /send reset/i }).click();
await expect(page.getByRole('alert')).toContainText(/check your email/i);
});
// Happy path: set new password via reset link
test('sets new password with valid reset token', async ({ page }) => {
await page.goto('{{baseUrl}}/reset-password?token={{resetToken}}');
await expect(page.getByRole('heading', { name: /set.*new password/i })).toBeVisible();
await page.getByRole('textbox', { name: /^new password$/i }).fill('{{newPassword}}');
await page.getByRole('textbox', { name: /confirm password/i }).fill('{{newPassword}}');
await page.getByRole('button', { name: /reset password/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/login');
await expect(page.getByRole('alert')).toContainText(/password.*updated/i);
});
// Happy path: login with new password
test('can log in with updated password', async ({ page }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{newPassword}}');
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
});
// Error case: expired reset link
test('shows error for expired reset token', async ({ page }) => {
await page.goto('{{baseUrl}}/reset-password?token={{expiredResetToken}}');
await expect(page.getByRole('alert')).toContainText(/link.*expired|token.*invalid/i);
await expect(page.getByRole('link', { name: /request new link/i })).toBeVisible();
});
// Error case: unknown email
test('shows generic message for unknown email (anti-enumeration)', async ({ page }) => {
await page.goto('{{baseUrl}}/forgot-password');
await page.getByRole('textbox', { name: /email/i }).fill('[email protected]');
await page.getByRole('button', { name: /send reset/i }).click();
// Should NOT reveal whether email exists
await expect(page.getByRole('alert')).toContainText(/check your email/i);
});
// Error case: passwords do not match
test('validates that passwords match', async ({ page }) => {
await page.goto('{{baseUrl}}/reset-password?token={{resetToken}}');
await page.getByRole('textbox', { name: /^new password$/i }).fill('{{newPassword}}');
await page.getByRole('textbox', { name: /confirm password/i }).fill('different-password');
await page.getByRole('button', { name: /reset password/i }).click();
await expect(page.getByText(/passwords.*do not match/i)).toBeVisible();
});
// Edge case: weak password rejected
test('rejects password that does not meet strength requirements', async ({ page }) => {
await page.goto('{{baseUrl}}/reset-password?token={{resetToken}}');
await page.getByRole('textbox', { name: /^new password$/i }).fill('123');
await page.getByRole('textbox', { name: /confirm password/i }).fill('123');
await page.getByRole('button', { name: /reset password/i }).click();
await expect(page.getByText(/password.*too weak|must be at least/i)).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Password Reset', () => {
test('sends reset email for known address', async ({ page }) => {
await page.goto('{{baseUrl}}/forgot-password');
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('button', { name: /send reset/i }).click();
await expect(page.getByRole('alert')).toContainText(/check your email/i);
});
test('sets new password with valid reset token', async ({ page }) => {
await page.goto('{{baseUrl}}/reset-password?token={{resetToken}}');
await page.getByRole('textbox', { name: /^new password$/i }).fill('{{newPassword}}');
await page.getByRole('textbox', { name: /confirm password/i }).fill('{{newPassword}}');
await page.getByRole('button', { name: /reset password/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/login');
});
test('shows error for expired reset token', async ({ page }) => {
await page.goto('{{baseUrl}}/reset-password?token={{expiredResetToken}}');
await expect(page.getByRole('alert')).toContainText(/link.*expired|token.*invalid/i);
});
test('validates passwords match', async ({ page }) => {
await page.goto('{{baseUrl}}/reset-password?token={{resetToken}}');
await page.getByRole('textbox', { name: /^new password$/i }).fill('{{newPassword}}');
await page.getByRole('textbox', { name: /confirm password/i }).fill('other');
await page.getByRole('button', { name: /reset password/i }).click();
await expect(page.getByText(/passwords.*do not match/i)).toBeVisible();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Request reset | Known email → check email message |
| Set new password | Valid token → new password set → login page |
| Login with new pw | Updated credentials accepted |
| Expired token | Error + "request new link" shown |
| Unknown email | Generic response (anti-enumeration) |
| Passwords mismatch | Inline validation error |
| Weak password | Strength requirement error |
FILE:templates/auth/rbac.md
# RBAC Template
Tests role-based access control: admin vs user permissions and forbidden pages.
## Prerequisites
- Admin account: `{{adminUsername}}` / `{{adminPassword}}`
- Regular user: `{{userUsername}}` / `{{userPassword}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
const adminState = '{{adminStorageStatePath}}';
const userState = '{{userStorageStatePath}}';
test.describe('RBAC — Admin', () => {
test.use({ storageState: adminState });
// Happy path: admin accesses admin panel
test('admin can access admin panel', async ({ page }) => {
await page.goto('{{baseUrl}}/admin');
await expect(page.getByRole('heading', { name: /admin/i })).toBeVisible();
});
test('admin can see user management menu item', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByRole('link', { name: /user management/i })).toBeVisible();
});
test('admin can delete any resource', async ({ page }) => {
await page.goto('{{baseUrl}}/admin/{{entityName}}s');
await page.getByRole('row').nth(1).getByRole('button', { name: /delete/i }).click();
await page.getByRole('button', { name: /confirm/i }).click();
await expect(page.getByRole('alert')).toContainText(/deleted/i);
});
});
test.describe('RBAC — Regular User', () => {
test.use({ storageState: userState });
// Error case: user cannot access admin panel
test('regular user sees 403 on admin panel', async ({ page }) => {
await page.goto('{{baseUrl}}/admin');
await expect(page).toHaveURL(/\/403|\/forbidden|\/dashboard/);
const forbidden = page.getByRole('heading', { name: /403|forbidden|not authorized/i });
await expect(forbidden).toBeVisible();
});
test('regular user does not see admin menu items', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByRole('link', { name: /user management/i })).toBeHidden();
});
// Error case: user cannot delete others' resources
test('regular user cannot delete another user\'s resource', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{otherUsersEntityId}}');
await expect(page.getByRole('button', { name: /delete/i })).toBeHidden();
});
// Edge case: direct navigation to admin API returns 403
test('API returns 403 for unauthorized role', async ({ page }) => {
const response = await page.request.get('{{baseUrl}}/api/admin/users');
expect(response.status()).toBe(403);
});
});
test.describe('RBAC — Role Elevation', () => {
// Edge case: user promoted to admin gains access
test('newly promoted admin can access admin panel', async ({ browser }) => {
// Step 1: use admin context to promote user
const adminCtx = await browser.newContext({ storageState: adminState });
const adminPage = await adminCtx.newPage();
await adminPage.goto('{{baseUrl}}/admin/users/{{promotedUserId}}/role');
await adminPage.getByRole('combobox', { name: /role/i }).selectOption('admin');
await adminPage.getByRole('button', { name: /save/i }).click();
await adminCtx.close();
// Step 2: promoted user can now access admin panel
const userCtx = await browser.newContext({ storageState: userState });
const userPage = await userCtx.newPage();
await userPage.goto('{{baseUrl}}/admin');
await expect(userPage.getByRole('heading', { name: /admin/i })).toBeVisible();
await userCtx.close();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('RBAC — Admin', () => {
test.use({ storageState: '{{adminStorageStatePath}}' });
test('admin can access admin panel', async ({ page }) => {
await page.goto('{{baseUrl}}/admin');
await expect(page.getByRole('heading', { name: /admin/i })).toBeVisible();
});
});
test.describe('RBAC — Regular User', () => {
test.use({ storageState: '{{userStorageStatePath}}' });
test('regular user sees 403 on admin panel', async ({ page }) => {
await page.goto('{{baseUrl}}/admin');
await expect(page.getByRole('heading', { name: /403|forbidden/i })).toBeVisible();
});
test('API returns 403 for unauthorized role', async ({ page }) => {
const res = await page.request.get('{{baseUrl}}/api/admin/users');
expect(res.status()).toBe(403);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Admin access | Admin reaches /admin panel |
| Admin menu | Admin-only nav items visible |
| Admin delete | Admin can delete any resource |
| User forbidden | Regular user → 403/redirect on /admin |
| User hidden menu | Admin nav items not rendered for user |
| API 403 | Backend enforces role on API routes |
| Role elevation | Promoted user gains new access immediately |
FILE:templates/auth/remember-me.md
# Remember Me Template
Tests persistent login cookie behaviour and expiry.
## Prerequisites
- Valid account: `{{username}}` / `{{password}}`
- `{{sessionCookieName}}` cookie used for auth
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Remember Me', () => {
// Happy path: cookie is long-lived when remember me is checked
test('sets persistent cookie when remember me is checked', async ({ page, context }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{password}}');
await page.getByRole('checkbox', { name: /remember me/i }).check();
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
const cookies = await context.cookies();
const session = cookies.find(c => c.name === '{{sessionCookieName}}');
// Cookie should expire > 7 days from now
expect(session?.expires).toBeGreaterThan(Date.now() / 1000 + 7 * 86400);
});
// Happy path: session cookie (no remember me) is session-scoped
test('sets session-scoped cookie when remember me is unchecked', async ({ page, context }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{password}}');
const checkbox = page.getByRole('checkbox', { name: /remember me/i });
if (await checkbox.isChecked()) await checkbox.uncheck();
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
const cookies = await context.cookies();
const session = cookies.find(c => c.name === '{{sessionCookieName}}');
// Session cookie: expires = -1 (browser session only)
expect(session?.expires).toBeLessThanOrEqual(0);
});
// Happy path: persistent login survives page reload
test('stays logged in across browser restart with remember me', async ({ page, context }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{password}}');
await page.getByRole('checkbox', { name: /remember me/i }).check();
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
// Simulate new browser session by closing & reopening page (cookies persist)
await page.close();
const newPage = await context.newPage();
await newPage.goto('{{baseUrl}}/dashboard');
await expect(newPage).toHaveURL('{{baseUrl}}/dashboard');
await expect(newPage.getByRole('heading', { name: /dashboard/i })).toBeVisible();
});
// Error case: expired persistent cookie redirects to login
test('redirects to login when persistent cookie has expired', async ({ page, context }) => {
await context.addCookies([{
name: '{{sessionCookieName}}',
value: '{{expiredCookieValue}}',
domain: '{{cookieDomain}}',
path: '/',
expires: Math.floor(Date.now() / 1000) - 1, // already expired
}]);
await page.goto('{{baseUrl}}/dashboard');
await expect(page).toHaveURL(/\/login/);
});
// Edge case: remember me checkbox state is preserved on validation error
test('retains remember me checkbox state after failed login', async ({ page }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('checkbox', { name: /remember me/i }).check();
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('wrong');
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page.getByRole('alert')).toContainText(/invalid/i);
await expect(page.getByRole('checkbox', { name: /remember me/i })).toBeChecked();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Remember Me', () => {
test('sets persistent cookie when remember me is checked', async ({ page, context }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{password}}');
await page.getByRole('checkbox', { name: /remember me/i }).check();
await page.getByRole('button', { name: /sign in/i }).click();
const cookies = await context.cookies();
const session = cookies.find(c => c.name === '{{sessionCookieName}}');
expect(session?.expires).toBeGreaterThan(Date.now() / 1000 + 7 * 86400);
});
test('sets session cookie when remember me is unchecked', async ({ page, context }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{password}}');
await page.getByRole('button', { name: /sign in/i }).click();
const cookies = await context.cookies();
const session = cookies.find(c => c.name === '{{sessionCookieName}}');
expect(session?.expires).toBeLessThanOrEqual(0);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Persistent cookie | Remember me → long-lived cookie (>7 days) |
| Session cookie | No remember me → session-scoped cookie |
| Survives reload | Persistent cookie keeps user logged in across restart |
| Expired cookie | Stale cookie → redirect to /login |
| Checkbox retained | State preserved after failed login attempt |
FILE:templates/auth/session-timeout.md
# Session Timeout Template
Tests auto-logout after inactivity and session refresh behaviour.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Session timeout configured to `{{sessionTimeoutMs}}` ms in test env
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Session Timeout', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
// Happy path: session refresh on activity
test('refreshes session on user activity', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.clock.install();
// Advance to just before timeout
await page.clock.fastForward({{sessionTimeoutMs}} - 5000);
await page.getByRole('button', { name: /any interactive element/i }).click();
// Advance past original timeout — session should still be valid
await page.clock.fastForward(10_000);
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
});
// Happy path: warning dialog shown before logout
test('shows session-expiry warning before auto-logout', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.clock.install();
await page.clock.fastForward({{sessionTimeoutMs}} - {{warningLeadMs}});
await expect(page.getByRole('dialog', { name: /session.*expiring/i })).toBeVisible();
await expect(page.getByRole('button', { name: /stay signed in/i })).toBeVisible();
});
// Happy path: extend session from warning dialog
test('extends session when "stay signed in" clicked', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.clock.install();
await page.clock.fastForward({{sessionTimeoutMs}} - {{warningLeadMs}});
await page.getByRole('button', { name: /stay signed in/i }).click();
await expect(page.getByRole('dialog', { name: /session.*expiring/i })).toBeHidden();
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
});
// Error case: auto-logout after inactivity
test('redirects to login after session timeout', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.clock.install();
await page.clock.fastForward({{sessionTimeoutMs}} + 1000);
await expect(page).toHaveURL(/\/login/);
await expect(page.getByText(/session.*expired|signed out/i)).toBeVisible();
});
// Edge case: API calls return 401 after timeout
test('shows re-auth prompt when API returns 401', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.route('{{baseUrl}}/api/**', route =>
route.fulfill({ status: 401, body: JSON.stringify({ error: 'Unauthorized' }) })
);
await page.getByRole('button', { name: /refresh|reload/i }).click();
await expect(page.getByRole('dialog', { name: /session.*expired/i })).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Session Timeout', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('shows warning before auto-logout', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.clock.install();
await page.clock.fastForward({{sessionTimeoutMs}} - {{warningLeadMs}});
await expect(page.getByRole('dialog', { name: /session.*expiring/i })).toBeVisible();
});
test('auto-logs out after inactivity', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.clock.install();
await page.clock.fastForward({{sessionTimeoutMs}} + 1000);
await expect(page).toHaveURL(/\/login/);
});
test('extends session on "stay signed in"', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.clock.install();
await page.clock.fastForward({{sessionTimeoutMs}} - {{warningLeadMs}});
await page.getByRole('button', { name: /stay signed in/i }).click();
await expect(page.getByRole('dialog', { name: /session.*expiring/i })).toBeHidden();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Session refresh | Activity before timeout resets the clock |
| Warning dialog | Shown N ms before timeout |
| Extend session | "Stay signed in" dismisses warning |
| Auto-logout | Inactivity past timeout → /login |
| 401 from API | Re-auth dialog shown when backend rejects request |
FILE:templates/auth/sso.md
# SSO Template
Tests SSO redirect flow, IdP callback handling, and attribute mapping.
## Prerequisites
- SSO provider configured (SAML / OIDC) at `{{ssoProviderUrl}}`
- Test IdP with user `{{ssoUsername}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect, Page } from '@playwright/test';
async function completeSsoLogin(page: Page, username: string): Promise<void> {
// Fill IdP login form — adapt selectors to your provider
await page.getByRole('textbox', { name: /username/i }).fill(username);
await page.getByRole('button', { name: /login/i }).click();
}
test.describe('SSO', () => {
// Happy path: SSO redirect and callback
test('redirects to IdP and returns authenticated', async ({ page }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('button', { name: /sign in with sso/i }).click();
await expect(page).toHaveURL(/{{ssoProviderDomain}}/);
await completeSsoLogin(page, '{{ssoUsername}}');
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
});
// Happy path: SSO with domain hint
test('pre-fills organisation domain and redirects', async ({ page }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('textbox', { name: /work email/i }).fill('{{ssoUsername}}');
await page.getByRole('button', { name: /continue/i }).click();
await expect(page).toHaveURL(/{{ssoProviderDomain}}/);
});
// Happy path: attributes mapped to user profile
test('maps SSO attributes to user profile', async ({ page }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('button', { name: /sign in with sso/i }).click();
await completeSsoLogin(page, '{{ssoUsername}}');
await page.goto('{{baseUrl}}/settings/profile');
await expect(page.getByRole('textbox', { name: /email/i })).toHaveValue('{{ssoUsername}}');
});
// Error case: IdP returns error
test('shows error page when IdP returns error response', async ({ page }) => {
await page.goto('{{baseUrl}}/auth/callback?error=access_denied&error_description=User+denied+access');
await expect(page.getByRole('alert')).toContainText(/access denied/i);
await expect(page.getByRole('link', { name: /back to login/i })).toBeVisible();
});
// Error case: invalid callback state
test('rejects callback with invalid state parameter', async ({ page }) => {
await page.goto('{{baseUrl}}/auth/callback?code=valid_code&state=tampered_state');
await expect(page.getByRole('alert')).toContainText(/invalid.*state|authentication failed/i);
});
// Edge case: SSO user first login provisions account
test('provisions new account on first SSO login', async ({ page }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('button', { name: /sign in with sso/i }).click();
await completeSsoLogin(page, '{{newSsoUsername}}');
await expect(page).toHaveURL(/{{baseUrl}}\/(dashboard|onboarding)/);
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
async function completeSsoLogin(page, username) {
await page.getByRole('textbox', { name: /username/i }).fill(username);
await page.getByRole('button', { name: /login/i }).click();
}
test.describe('SSO', () => {
test('redirects to IdP and returns authenticated', async ({ page }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('button', { name: /sign in with sso/i }).click();
await expect(page).toHaveURL(/{{ssoProviderDomain}}/);
await completeSsoLogin(page, '{{ssoUsername}}');
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
});
test('shows error when IdP returns access_denied', async ({ page }) => {
await page.goto('{{baseUrl}}/auth/callback?error=access_denied');
await expect(page.getByRole('alert')).toContainText(/access denied/i);
});
test('rejects tampered state parameter', async ({ page }) => {
await page.goto('{{baseUrl}}/auth/callback?code=abc&state=tampered');
await expect(page.getByRole('alert')).toContainText(/invalid.*state|authentication failed/i);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Happy path | SSO button → IdP → callback → dashboard |
| Domain hint | Email triggers org-specific IdP redirect |
| Attribute mapping | SSO profile fields populate user record |
| IdP error | access_denied → error page with back link |
| Invalid state | CSRF protection rejects tampered callback |
| First login | Auto-provisions account on initial SSO |
FILE:templates/checkout/add-to-cart.md
# Add to Cart Template
Tests adding items to cart and quantity updates.
## Prerequisites
- Authenticated (or guest) session
- Product: ID `{{productId}}`, name `{{productName}}`, price `{{productPrice}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Add to Cart', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/products/{{productId}}');
});
// Happy path: add single item
test('adds product to cart', async ({ page }) => {
await page.getByRole('button', { name: /add to cart/i }).click();
await expect(page.getByRole('status', { name: /cart/i })).toContainText('1');
await expect(page.getByRole('alert')).toContainText(/added to cart/i);
});
// Happy path: add multiple items increments count
test('increments cart count on repeated add', async ({ page }) => {
await page.getByRole('button', { name: /add to cart/i }).click();
await page.getByRole('button', { name: /add to cart/i }).click();
await expect(page.getByRole('status', { name: /cart/i })).toContainText('2');
});
// Happy path: add with quantity selector
test('adds specified quantity to cart', async ({ page }) => {
await page.getByRole('spinbutton', { name: /quantity/i }).fill('3');
await page.getByRole('button', { name: /add to cart/i }).click();
await expect(page.getByRole('status', { name: /cart/i })).toContainText('3');
});
// Happy path: cart persists on navigation
test('cart persists after navigating away', async ({ page }) => {
await page.getByRole('button', { name: /add to cart/i }).click();
await page.goto('{{baseUrl}}/products');
await expect(page.getByRole('status', { name: /cart/i })).toContainText('1');
});
// Error case: out of stock product cannot be added
test('add to cart button disabled for out-of-stock product', async ({ page }) => {
await page.goto('{{baseUrl}}/products/{{outOfStockProductId}}');
await expect(page.getByRole('button', { name: /add to cart/i })).toBeDisabled();
await expect(page.getByText(/out of stock/i)).toBeVisible();
});
// Error case: quantity exceeds stock
test('shows error when quantity exceeds available stock', async ({ page }) => {
await page.getByRole('spinbutton', { name: /quantity/i }).fill('{{overStockQuantity}}');
await page.getByRole('button', { name: /add to cart/i }).click();
await expect(page.getByRole('alert')).toContainText(/only.*available|exceeds.*stock/i);
});
// Edge case: cart opens after add
test('cart drawer opens after adding item', async ({ page }) => {
await page.getByRole('button', { name: /add to cart/i }).click();
await expect(page.getByRole('dialog', { name: /cart/i })).toBeVisible();
await expect(page.getByRole('dialog').getByText('{{productName}}')).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Add to Cart', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/products/{{productId}}');
});
test('adds product to cart', async ({ page }) => {
await page.getByRole('button', { name: /add to cart/i }).click();
await expect(page.getByRole('status', { name: /cart/i })).toContainText('1');
});
test('add to cart disabled for out-of-stock', async ({ page }) => {
await page.goto('{{baseUrl}}/products/{{outOfStockProductId}}');
await expect(page.getByRole('button', { name: /add to cart/i })).toBeDisabled();
});
test('cart persists after navigation', async ({ page }) => {
await page.getByRole('button', { name: /add to cart/i }).click();
await page.goto('{{baseUrl}}/products');
await expect(page.getByRole('status', { name: /cart/i })).toContainText('1');
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Single add | Product added, cart count = 1 |
| Repeated add | Cart count increments |
| Quantity selector | Specified quantity added |
| Persist on nav | Cart count survives page change |
| Out of stock | Button disabled, label shown |
| Quantity exceeds stock | Error alert |
| Cart drawer | Slide-in cart opens showing added item |
FILE:templates/checkout/apply-coupon.md
# Apply Coupon Template
Tests valid coupon code, invalid code, and expired coupon handling.
## Prerequisites
- Cart with items totalling `{{cartTotal}}`
- Valid coupon: `{{validCouponCode}}` ({{discountPercent}}% off)
- Expired coupon: `{{expiredCouponCode}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Apply Coupon', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/cart');
});
// Happy path: valid coupon applied
test('applies valid coupon and shows discount', async ({ page }) => {
await page.getByRole('textbox', { name: /coupon|promo code/i }).fill('{{validCouponCode}}');
await page.getByRole('button', { name: /apply/i }).click();
await expect(page.getByText(/{{discountPercent}}%.*off|discount applied/i)).toBeVisible();
await expect(page.getByText('{{discountedTotal}}')).toBeVisible();
await expect(page.getByRole('button', { name: /remove coupon/i })).toBeVisible();
});
// Happy path: percentage discount calculated correctly
test('calculates discount amount correctly', async ({ page }) => {
await page.getByRole('textbox', { name: /coupon|promo code/i }).fill('{{validCouponCode}}');
await page.getByRole('button', { name: /apply/i }).click();
const discountLine = page.getByRole('row', { name: /discount/i });
await expect(discountLine).toContainText('-{{discountAmount}}');
});
// Happy path: remove applied coupon
test('removes applied coupon and restores original total', async ({ page }) => {
await page.getByRole('textbox', { name: /coupon|promo code/i }).fill('{{validCouponCode}}');
await page.getByRole('button', { name: /apply/i }).click();
await page.getByRole('button', { name: /remove coupon/i }).click();
await expect(page.getByText('{{cartTotal}}')).toBeVisible();
await expect(page.getByRole('button', { name: /remove coupon/i })).toBeHidden();
});
// Error case: invalid coupon code
test('shows error for invalid coupon code', async ({ page }) => {
await page.getByRole('textbox', { name: /coupon|promo code/i }).fill('INVALID123');
await page.getByRole('button', { name: /apply/i }).click();
await expect(page.getByRole('alert')).toContainText(/invalid.*coupon|code not found/i);
await expect(page.getByText('{{cartTotal}}')).toBeVisible();
});
// Error case: expired coupon
test('shows error for expired coupon', async ({ page }) => {
await page.getByRole('textbox', { name: /coupon|promo code/i }).fill('{{expiredCouponCode}}');
await page.getByRole('button', { name: /apply/i }).click();
await expect(page.getByRole('alert')).toContainText(/expired|no longer valid/i);
});
// Error case: coupon not applicable to cart items
test('shows error when coupon excludes cart products', async ({ page }) => {
await page.getByRole('textbox', { name: /coupon|promo code/i }).fill('{{categoryRestrictedCoupon}}');
await page.getByRole('button', { name: /apply/i }).click();
await expect(page.getByRole('alert')).toContainText(/not applicable|excluded/i);
});
// Edge case: empty coupon field
test('apply button disabled when coupon field is empty', async ({ page }) => {
const applyBtn = page.getByRole('button', { name: /apply/i });
await expect(applyBtn).toBeDisabled();
await page.getByRole('textbox', { name: /coupon|promo code/i }).fill('X');
await expect(applyBtn).toBeEnabled();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Apply Coupon', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/cart');
});
test('applies valid coupon and shows discount', async ({ page }) => {
await page.getByRole('textbox', { name: /coupon|promo code/i }).fill('{{validCouponCode}}');
await page.getByRole('button', { name: /apply/i }).click();
await expect(page.getByText(/discount applied/i)).toBeVisible();
await expect(page.getByText('{{discountedTotal}}')).toBeVisible();
});
test('shows error for invalid coupon', async ({ page }) => {
await page.getByRole('textbox', { name: /coupon|promo code/i }).fill('INVALID123');
await page.getByRole('button', { name: /apply/i }).click();
await expect(page.getByRole('alert')).toContainText(/invalid.*coupon/i);
});
test('shows error for expired coupon', async ({ page }) => {
await page.getByRole('textbox', { name: /coupon|promo code/i }).fill('{{expiredCouponCode}}');
await page.getByRole('button', { name: /apply/i }).click();
await expect(page.getByRole('alert')).toContainText(/expired/i);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Valid coupon | Discount applied, total updated |
| Discount calculation | Discount line shows correct amount |
| Remove coupon | Original total restored |
| Invalid code | Error alert, total unchanged |
| Expired coupon | Expiry error shown |
| Category restriction | Coupon not applicable error |
| Empty field | Apply button disabled |
FILE:templates/checkout/order-confirm.md
# Order Confirmation Template
Tests the success page and order details after checkout.
## Prerequisites
- Completed order with ID `{{orderId}}`
- Authenticated session via `{{authStorageStatePath}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Order Confirmation', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
// Happy path: confirmation page content
test('shows order confirmation with correct details', async ({ page }) => {
await page.goto('{{baseUrl}}/order-confirmation/{{orderId}}');
await expect(page.getByRole('heading', { name: /order confirmed|thank you/i })).toBeVisible();
await expect(page.getByText('{{orderId}}')).toBeVisible();
await expect(page.getByText('{{productName}}')).toBeVisible();
await expect(page.getByText('{{orderTotal}}')).toBeVisible();
});
// Happy path: confirmation email notice
test('shows confirmation email notice', async ({ page }) => {
await page.goto('{{baseUrl}}/order-confirmation/{{orderId}}');
await expect(page.getByText(/confirmation.*sent to|email.*{{username}}/i)).toBeVisible();
});
// Happy path: billing and shipping details shown
test('displays shipping address on confirmation page', async ({ page }) => {
await page.goto('{{baseUrl}}/order-confirmation/{{orderId}}');
await expect(page.getByText('{{shippingAddress}}')).toBeVisible();
await expect(page.getByText('{{billingAddress}}')).toBeVisible();
});
// Happy path: CTA navigates to order history
test('"view your orders" link navigates to order history', async ({ page }) => {
await page.goto('{{baseUrl}}/order-confirmation/{{orderId}}');
await page.getByRole('link', { name: /view.*orders|my orders/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/orders');
});
// Happy path: continue shopping CTA
test('"continue shopping" returns to products', async ({ page }) => {
await page.goto('{{baseUrl}}/order-confirmation/{{orderId}}');
await page.getByRole('link', { name: /continue shopping/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/products');
});
// Error case: accessing another user's order shows 403
test('cannot access another user\'s confirmation page', async ({ page }) => {
await page.goto('{{baseUrl}}/order-confirmation/{{otherUsersOrderId}}');
await expect(page).toHaveURL(/\/403|\/dashboard/);
});
// Edge case: cart is empty after successful checkout
test('cart is empty after order confirmed', async ({ page }) => {
await page.goto('{{baseUrl}}/order-confirmation/{{orderId}}');
await expect(page.getByRole('status', { name: /cart/i })).toContainText('0');
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Order Confirmation', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('shows order id and total on confirmation', async ({ page }) => {
await page.goto('{{baseUrl}}/order-confirmation/{{orderId}}');
await expect(page.getByRole('heading', { name: /order confirmed|thank you/i })).toBeVisible();
await expect(page.getByText('{{orderId}}')).toBeVisible();
await expect(page.getByText('{{orderTotal}}')).toBeVisible();
});
test('cart is empty after checkout', async ({ page }) => {
await page.goto('{{baseUrl}}/order-confirmation/{{orderId}}');
await expect(page.getByRole('status', { name: /cart/i })).toContainText('0');
});
test('cannot access another user\'s order', async ({ page }) => {
await page.goto('{{baseUrl}}/order-confirmation/{{otherUsersOrderId}}');
await expect(page).toHaveURL(/\/403|\/dashboard/);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Confirmation content | Order ID, product, total visible |
| Email notice | Confirmation email address shown |
| Shipping/billing | Addresses displayed |
| View orders CTA | Navigates to /orders |
| Continue shopping | Returns to /products |
| Unauthorized | Other user's order → 403 |
| Cart cleared | Cart count = 0 after checkout |
FILE:templates/checkout/order-history.md
# Order History Template
Tests listing orders, viewing order details, and pagination.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- At least `{{orderCount}}` orders seeded for user
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Order History', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
// Happy path: order list
test('displays list of orders with key details', async ({ page }) => {
await page.goto('{{baseUrl}}/orders');
await expect(page.getByRole('heading', { name: /orders|order history/i })).toBeVisible();
const rows = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
await expect(rows.first()).toContainText('{{latestOrderId}}');
await expect(rows.first()).toContainText('{{latestOrderStatus}}');
await expect(rows.first()).toContainText('{{latestOrderTotal}}');
});
// Happy path: view order details
test('navigates to order detail from history', async ({ page }) => {
await page.goto('{{baseUrl}}/orders');
await page.getByRole('link', { name: new RegExp('{{latestOrderId}}') }).click();
await expect(page).toHaveURL(`{{baseUrl}}/orders/{{latestOrderId}}`);
await expect(page.getByRole('heading', { name: '{{latestOrderId}}' })).toBeVisible();
await expect(page.getByText('{{productName}}')).toBeVisible();
});
// Happy path: order status badge
test('shows correct status badge for each order', async ({ page }) => {
await page.goto('{{baseUrl}}/orders');
const deliveredBadge = page.getByRole('status', { name: /delivered/i }).first();
await expect(deliveredBadge).toBeVisible();
});
// Happy path: pagination
test('paginates through orders', async ({ page }) => {
await page.goto('{{baseUrl}}/orders');
const firstPageFirstOrder = await page.getByRole('row').nth(1).textContent();
await page.getByRole('button', { name: /next page|>/i }).click();
await expect(page.getByRole('row').nth(1)).not.toHaveText(firstPageFirstOrder!);
await expect(page.getByRole('button', { name: /previous page|</i })).toBeEnabled();
});
// Happy path: items per page selector
test('changes items per page', async ({ page }) => {
await page.goto('{{baseUrl}}/orders');
await page.getByRole('combobox', { name: /per page|items per page/i }).selectOption('50');
const rows = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
await expect(rows).toHaveCount(Math.min(50, {{orderCount}}));
});
// Error case: empty order history
test('shows empty state for user with no orders', async ({ page }) => {
await page.goto('{{baseUrl}}/orders');
// Assumes this user context has no orders
await expect(page.getByText(/no orders yet|start shopping/i)).toBeVisible();
});
// Edge case: reorder from history
test('adds previous order items to cart via reorder', async ({ page }) => {
await page.goto('{{baseUrl}}/orders/{{latestOrderId}}');
await page.getByRole('button', { name: /reorder|buy again/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/cart');
await expect(page.getByText('{{productName}}')).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Order History', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('displays orders with id, status, and total', async ({ page }) => {
await page.goto('{{baseUrl}}/orders');
const rows = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
await expect(rows.first()).toContainText('{{latestOrderId}}');
});
test('navigates to order detail', async ({ page }) => {
await page.goto('{{baseUrl}}/orders');
await page.getByRole('link', { name: new RegExp('{{latestOrderId}}') }).click();
await expect(page).toHaveURL(`{{baseUrl}}/orders/{{latestOrderId}}`);
});
test('paginates through orders', async ({ page }) => {
await page.goto('{{baseUrl}}/orders');
await page.getByRole('button', { name: /next page|>/i }).click();
await expect(page.getByRole('button', { name: /previous page|</i })).toBeEnabled();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Order list | ID, status, total visible per row |
| Order detail | Clicking order → detail page |
| Status badge | Correct badge per order state |
| Pagination | Next page loads different orders |
| Items per page | Selector changes row count |
| Empty state | No-orders message with CTA |
| Reorder | Previous order items added to cart |
FILE:templates/checkout/payment.md
# Payment Template
Tests card form entry, validation, and payment processing.
## Prerequisites
- Cart with items, shipping filled
- Test card numbers: `{{testCardNumber}}` (success), `{{declinedCardNumber}}` (decline)
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect, Page } from '@playwright/test';
async function fillCardForm(page: Page, card: {
number: string; expiry: string; cvc: string; name: string;
}): Promise<void> {
// Stripe/Braintree iframes — adapt frame locator to your provider
const cardFrame = page.frameLocator('[data-testid="card-number-frame"]');
await cardFrame.getByRole('textbox', { name: /card number/i }).fill(card.number);
const expiryFrame = page.frameLocator('[data-testid="expiry-frame"]');
await expiryFrame.getByRole('textbox', { name: /expiry/i }).fill(card.expiry);
const cvcFrame = page.frameLocator('[data-testid="cvc-frame"]');
await cvcFrame.getByRole('textbox', { name: /cvc|cvv/i }).fill(card.cvc);
await page.getByRole('textbox', { name: /cardholder name/i }).fill(card.name);
}
test.describe('Payment', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/checkout/payment');
});
// Happy path: successful payment
test('completes payment with valid card', async ({ page }) => {
await fillCardForm(page, {
number: '{{testCardNumber}}',
expiry: '12/28',
cvc: '123',
name: '{{cardholderName}}',
});
await page.getByRole('button', { name: /pay|place order/i }).click();
await expect(page).toHaveURL(/\/order-confirmation|\/success/);
await expect(page.getByRole('heading', { name: /order confirmed|thank you/i })).toBeVisible();
});
// Happy path: processing state shown
test('shows processing state while payment is pending', async ({ page }) => {
await fillCardForm(page, {
number: '{{testCardNumber}}',
expiry: '12/28',
cvc: '123',
name: '{{cardholderName}}',
});
const payBtn = page.getByRole('button', { name: /pay|place order/i });
await payBtn.click();
await expect(payBtn).toBeDisabled();
await expect(page.getByText(/processing|please wait/i)).toBeVisible();
});
// Error case: declined card
test('shows decline error for rejected card', async ({ page }) => {
await fillCardForm(page, {
number: '{{declinedCardNumber}}',
expiry: '12/28',
cvc: '123',
name: '{{cardholderName}}',
});
await page.getByRole('button', { name: /pay|place order/i }).click();
await expect(page.getByRole('alert')).toContainText(/declined|card.*not accepted/i);
await expect(page).toHaveURL(/\/checkout\/payment/);
});
// Error case: invalid card number format
test('shows inline error for invalid card number', async ({ page }) => {
const cardFrame = page.frameLocator('[data-testid="card-number-frame"]');
await cardFrame.getByRole('textbox', { name: /card number/i }).fill('1234');
await page.getByRole('button', { name: /pay|place order/i }).click();
await expect(page.getByText(/invalid.*card number/i)).toBeVisible();
});
// Error case: expired card
test('shows error for expired card', async ({ page }) => {
await fillCardForm(page, {
number: '{{testCardNumber}}',
expiry: '01/20',
cvc: '123',
name: '{{cardholderName}}',
});
await page.getByRole('button', { name: /pay|place order/i }).click();
await expect(page.getByRole('alert')).toContainText(/expired|invalid.*expiry/i);
});
// Edge case: 3DS authentication required
test('handles 3DS challenge and completes payment', async ({ page }) => {
await fillCardForm(page, {
number: '{{threeDsCardNumber}}',
expiry: '12/28',
cvc: '123',
name: '{{cardholderName}}',
});
await page.getByRole('button', { name: /pay|place order/i }).click();
// 3DS modal appears
const challengeFrame = page.frameLocator('[data-testid="3ds-challenge-frame"]');
await challengeFrame.getByRole('button', { name: /complete authentication/i }).click();
await expect(page).toHaveURL(/\/order-confirmation|\/success/);
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Payment', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/checkout/payment');
});
test('completes payment with valid card', async ({ page }) => {
const cardFrame = page.frameLocator('[data-testid="card-number-frame"]');
await cardFrame.getByRole('textbox', { name: /card number/i }).fill('{{testCardNumber}}');
await page.getByRole('button', { name: /pay|place order/i }).click();
await expect(page).toHaveURL(/\/order-confirmation/);
});
test('shows decline error for rejected card', async ({ page }) => {
const cardFrame = page.frameLocator('[data-testid="card-number-frame"]');
await cardFrame.getByRole('textbox', { name: /card number/i }).fill('{{declinedCardNumber}}');
await page.getByRole('button', { name: /pay|place order/i }).click();
await expect(page.getByRole('alert')).toContainText(/declined/i);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Successful payment | Valid test card → order confirmation |
| Processing state | Button disabled + spinner during processing |
| Declined card | Error alert, stays on payment page |
| Invalid card number | Inline validation from provider |
| Expired card | Expiry error |
| 3DS challenge | Modal completed, payment succeeds |
FILE:templates/checkout/update-quantity.md
# Update Cart Quantity Template
Tests increasing, decreasing, and removing items from cart.
## Prerequisites
- Cart with at least one item: `{{productName}}` (quantity 2)
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Update Cart Quantity', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/cart');
// Assumes cart is pre-populated via storageState or API setup
});
// Happy path: increase quantity
test('increases item quantity', async ({ page }) => {
const row = page.getByRole('row', { name: new RegExp('{{productName}}') });
await row.getByRole('button', { name: /increase|plus|\+/i }).click();
await expect(row.getByRole('spinbutton', { name: /quantity/i })).toHaveValue('3');
await expect(page.getByRole('region', { name: /order summary/i })).toContainText('{{updatedTotal}}');
});
// Happy path: decrease quantity
test('decreases item quantity', async ({ page }) => {
const row = page.getByRole('row', { name: new RegExp('{{productName}}') });
await row.getByRole('button', { name: /decrease|minus|−/i }).click();
await expect(row.getByRole('spinbutton', { name: /quantity/i })).toHaveValue('1');
});
// Happy path: type quantity directly
test('updates quantity by typing in field', async ({ page }) => {
const row = page.getByRole('row', { name: new RegExp('{{productName}}') });
const qtyInput = row.getByRole('spinbutton', { name: /quantity/i });
await qtyInput.fill('5');
await qtyInput.press('Tab');
await expect(qtyInput).toHaveValue('5');
});
// Happy path: remove item with remove button
test('removes item from cart', async ({ page }) => {
const row = page.getByRole('row', { name: new RegExp('{{productName}}') });
await row.getByRole('button', { name: /remove|delete/i }).click();
await expect(row).toBeHidden();
await expect(page.getByText(/cart is empty/i)).toBeVisible();
});
// Happy path: decrease to 0 removes item
test('removing to quantity 0 removes item', async ({ page }) => {
const row = page.getByRole('row', { name: new RegExp('{{productName}}') });
await row.getByRole('button', { name: /decrease|minus/i }).click(); // from 2 to 1
await row.getByRole('button', { name: /decrease|minus/i }).click(); // should trigger remove
await expect(row).toBeHidden();
});
// Error case: quantity cannot go below 1 via decrease button
test('decrease button disabled at minimum quantity', async ({ page }) => {
const row = page.getByRole('row').nth(1);
const qty = row.getByRole('spinbutton', { name: /quantity/i });
await qty.fill('1');
await qty.press('Tab');
await expect(row.getByRole('button', { name: /decrease|minus/i })).toBeDisabled();
});
// Edge case: quantity clamped to stock limit
test('quantity capped at available stock', async ({ page }) => {
const row = page.getByRole('row', { name: new RegExp('{{productName}}') });
const qtyInput = row.getByRole('spinbutton', { name: /quantity/i });
await qtyInput.fill('{{overStockQuantity}}');
await qtyInput.press('Tab');
await expect(qtyInput).toHaveValue('{{maxStock}}');
await expect(page.getByRole('alert')).toContainText(/max.*available|stock limit/i);
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Update Cart Quantity', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/cart');
});
test('increases item quantity', async ({ page }) => {
const row = page.getByRole('row', { name: new RegExp('{{productName}}') });
await row.getByRole('button', { name: /increase|plus|\+/i }).click();
await expect(row.getByRole('spinbutton', { name: /quantity/i })).toHaveValue('3');
});
test('removes item from cart', async ({ page }) => {
await page.getByRole('row', { name: new RegExp('{{productName}}') })
.getByRole('button', { name: /remove|delete/i }).click();
await expect(page.getByText(/cart is empty/i)).toBeVisible();
});
test('decrease button disabled at quantity 1', async ({ page }) => {
const row = page.getByRole('row').nth(1);
await row.getByRole('spinbutton', { name: /quantity/i }).fill('1');
await row.getByRole('spinbutton', { name: /quantity/i }).press('Tab');
await expect(row.getByRole('button', { name: /decrease|minus/i })).toBeDisabled();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Increase | +1 → quantity updates, total recalculates |
| Decrease | -1 → quantity updates |
| Type directly | Manual quantity input accepted on blur/tab |
| Remove button | Item removed, empty-cart message shown |
| Decrease to 0 | Triggers item removal |
| Min quantity | Decrease button disabled at 1 |
| Stock cap | Input clamped to available stock |
FILE:templates/crud/bulk-operations.md
# Bulk Operations Template
Tests selecting multiple items and performing bulk delete/update actions.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- At least `{{minItemCount}}` entities seeded in list
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Bulk Operations', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s');
});
// Happy path: select all and bulk delete
test('selects all and bulk deletes', async ({ page }) => {
await page.getByRole('checkbox', { name: /select all/i }).check();
const checkboxes = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') })
.getByRole('checkbox');
await expect(checkboxes.first()).toBeChecked();
await page.getByRole('button', { name: /bulk delete/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /confirm/i }).click();
await expect(page.getByRole('alert')).toContainText(/deleted/i);
await expect(page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') }))
.toHaveCount(0);
});
// Happy path: select specific rows and bulk update status
test('updates status of selected rows', async ({ page }) => {
const rows = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
await rows.nth(0).getByRole('checkbox').check();
await rows.nth(1).getByRole('checkbox').check();
await expect(page.getByText(/2 selected/i)).toBeVisible();
await page.getByRole('button', { name: /bulk actions/i }).click();
await page.getByRole('menuitem', { name: /mark as active/i }).click();
await expect(page.getByRole('alert')).toContainText(/2.*updated/i);
});
// Happy path: toolbar appears only when items selected
test('shows bulk action toolbar only when items are selected', async ({ page }) => {
await expect(page.getByRole('toolbar', { name: /bulk actions/i })).toBeHidden();
await page.getByRole('row').nth(1).getByRole('checkbox').check();
await expect(page.getByRole('toolbar', { name: /bulk actions/i })).toBeVisible();
});
// Happy path: deselect all clears toolbar
test('hides toolbar after deselecting all', async ({ page }) => {
await page.getByRole('checkbox', { name: /select all/i }).check();
await page.getByRole('checkbox', { name: /select all/i }).uncheck();
await expect(page.getByRole('toolbar', { name: /bulk actions/i })).toBeHidden();
});
// Error case: bulk delete requires confirmation
test('requires confirmation before bulk delete', async ({ page }) => {
await page.getByRole('checkbox', { name: /select all/i }).check();
await page.getByRole('button', { name: /bulk delete/i }).click();
await expect(page.getByRole('dialog', { name: /confirm/i })).toBeVisible();
await page.getByRole('button', { name: /cancel/i }).click();
const rowCount = await page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') }).count();
expect(rowCount).toBeGreaterThan(0);
});
// Edge case: select all across pages
test('shows "select all across pages" option when applicable', async ({ page }) => {
await page.getByRole('checkbox', { name: /select all/i }).check();
const crossPage = page.getByRole('button', { name: /select all.*across pages/i });
if (await crossPage.isVisible()) {
await crossPage.click();
await expect(page.getByText(/all.*selected/i)).toBeVisible();
}
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Bulk Operations', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s');
});
test('shows bulk action toolbar when items selected', async ({ page }) => {
await expect(page.getByRole('toolbar', { name: /bulk actions/i })).toBeHidden();
await page.getByRole('row').nth(1).getByRole('checkbox').check();
await expect(page.getByRole('toolbar', { name: /bulk actions/i })).toBeVisible();
});
test('selects all and bulk deletes', async ({ page }) => {
await page.getByRole('checkbox', { name: /select all/i }).check();
await page.getByRole('button', { name: /bulk delete/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /confirm/i }).click();
await expect(page.getByRole('alert')).toContainText(/deleted/i);
});
test('requires confirmation before bulk delete', async ({ page }) => {
await page.getByRole('checkbox', { name: /select all/i }).check();
await page.getByRole('button', { name: /bulk delete/i }).click();
await expect(page.getByRole('dialog', { name: /confirm/i })).toBeVisible();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Select all + delete | All rows selected → confirmed delete → empty list |
| Partial select + update | N rows selected → status updated → success |
| Toolbar visibility | Appears on select, hides on deselect |
| Deselect all | Select all → uncheck → toolbar gone |
| Confirmation required | Bulk delete shows dialog first |
| Cross-page select | Select-all-pages option shown on multi-page lists |
FILE:templates/crud/create.md
# Create Entity Template
Tests creating a new entity via form submission.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Entity type: `{{entityName}}` (e.g. "Project", "Product", "User")
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Create {{entityName}}', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/new');
});
// Happy path: create with valid data
test('creates {{entityName}} with valid data', async ({ page }) => {
await page.getByRole('textbox', { name: /name/i }).fill('{{testEntityName}}');
await page.getByRole('textbox', { name: /description/i }).fill('{{testEntityDescription}}');
await page.getByRole('combobox', { name: /category/i }).selectOption('{{testEntityCategory}}');
await page.getByRole('button', { name: /create|save/i }).click();
await expect(page).toHaveURL(/\/{{entityName}}s\/\d+/);
await expect(page.getByRole('heading', { name: '{{testEntityName}}' })).toBeVisible();
await expect(page.getByRole('alert')).toContainText(/created successfully/i);
});
// Happy path: create and add another
test('clears form after "save and add another"', async ({ page }) => {
await page.getByRole('textbox', { name: /name/i }).fill('{{testEntityName}}');
await page.getByRole('button', { name: /save and add another/i }).click();
await expect(page.getByRole('textbox', { name: /name/i })).toHaveValue('');
await expect(page.getByRole('alert')).toContainText(/created successfully/i);
});
// Error case: required fields missing
test('shows validation errors for empty required fields', async ({ page }) => {
await page.getByRole('button', { name: /create|save/i }).click();
await expect(page.getByText(/name is required/i)).toBeVisible();
await expect(page).toHaveURL('{{baseUrl}}/{{entityName}}s/new');
});
// Error case: duplicate name
test('shows error when entity name already exists', async ({ page }) => {
await page.getByRole('textbox', { name: /name/i }).fill('{{existingEntityName}}');
await page.getByRole('button', { name: /create|save/i }).click();
await expect(page.getByRole('alert')).toContainText(/already exists|duplicate/i);
});
// Edge case: max length enforcement
test('enforces max length on name field', async ({ page }) => {
const longName = 'A'.repeat({{maxNameLength}} + 1);
await page.getByRole('textbox', { name: /name/i }).fill(longName);
const actualValue = await page.getByRole('textbox', { name: /name/i }).inputValue();
expect(actualValue.length).toBeLessThanOrEqual({{maxNameLength}});
});
// Edge case: cancel navigates away without saving
test('cancel navigates back without creating', async ({ page }) => {
await page.getByRole('textbox', { name: /name/i }).fill('should-not-save');
await page.getByRole('button', { name: /cancel/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/{{entityName}}s');
await expect(page.getByRole('cell', { name: 'should-not-save' })).toBeHidden();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Create {{entityName}}', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/new');
});
test('creates entity with valid data', async ({ page }) => {
await page.getByRole('textbox', { name: /name/i }).fill('{{testEntityName}}');
await page.getByRole('textbox', { name: /description/i }).fill('{{testEntityDescription}}');
await page.getByRole('button', { name: /create|save/i }).click();
await expect(page).toHaveURL(/\/{{entityName}}s\/\d+/);
await expect(page.getByRole('alert')).toContainText(/created successfully/i);
});
test('shows validation errors for empty form', async ({ page }) => {
await page.getByRole('button', { name: /create|save/i }).click();
await expect(page.getByText(/name is required/i)).toBeVisible();
});
test('cancel navigates back without saving', async ({ page }) => {
await page.getByRole('textbox', { name: /name/i }).fill('not-saved');
await page.getByRole('button', { name: /cancel/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/{{entityName}}s');
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Happy path | Valid form → entity created → detail page |
| Save and add | Form cleared, ready for next entry |
| Required fields | Empty submit → inline validation |
| Duplicate name | Server error shown |
| Max length | Input truncated at field max |
| Cancel | No entity created, returns to list |
FILE:templates/crud/delete.md
# Delete Entity Template
Tests deletion with confirmation dialog and post-delete behaviour.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Entity to delete: ID `{{entityId}}`, name `{{entityName}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Delete {{entityName}}', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
// Happy path: delete from detail page
test('deletes entity after confirming dialog', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
await page.getByRole('button', { name: /delete/i }).click();
const dialog = page.getByRole('dialog', { name: /delete|confirm/i });
await expect(dialog).toBeVisible();
await expect(dialog).toContainText('{{entityName}}');
await dialog.getByRole('button', { name: /delete|confirm/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/{{entityName}}s');
await expect(page.getByRole('alert')).toContainText(/deleted successfully/i);
await expect(page.getByRole('link', { name: '{{entityName}}' })).toBeHidden();
});
// Happy path: delete from list view
test('deletes entity from list row action', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s');
const row = page.getByRole('row', { name: new RegExp('{{entityName}}') });
await row.getByRole('button', { name: /delete/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /confirm|delete/i }).click();
await expect(row).toBeHidden();
});
// Error case: cancel deletion
test('does not delete when cancel is clicked in dialog', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
await page.getByRole('button', { name: /delete/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /cancel/i }).click();
await expect(page.getByRole('dialog')).toBeHidden();
await expect(page).toHaveURL(`{{baseUrl}}/{{entityName}}s/{{entityId}}`);
await expect(page.getByRole('heading', { name: '{{entityName}}' })).toBeVisible();
});
// Error case: delete entity with dependents
test('shows error when entity has dependent records', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityWithDependentsId}}');
await page.getByRole('button', { name: /delete/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /confirm|delete/i }).click();
await expect(page.getByRole('alert')).toContainText(/cannot delete|has dependents/i);
await expect(page).toHaveURL(`{{baseUrl}}/{{entityName}}s/{{entityWithDependentsId}}`);
});
// Edge case: confirmation dialog requires typing entity name
test('requires typing entity name to confirm deletion', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
await page.getByRole('button', { name: /delete/i }).click();
const confirmBtn = page.getByRole('dialog').getByRole('button', { name: /confirm|delete/i });
await expect(confirmBtn).toBeDisabled();
await page.getByRole('textbox', { name: /type.*to confirm/i }).fill('{{entityName}}');
await expect(confirmBtn).toBeEnabled();
await confirmBtn.click();
await expect(page).toHaveURL('{{baseUrl}}/{{entityName}}s');
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Delete {{entityName}}', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('deletes entity after confirming dialog', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
await page.getByRole('button', { name: /delete/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /confirm|delete/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/{{entityName}}s');
await expect(page.getByRole('alert')).toContainText(/deleted successfully/i);
});
test('does not delete when cancel clicked', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
await page.getByRole('button', { name: /delete/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /cancel/i }).click();
await expect(page.getByRole('heading', { name: '{{entityName}}' })).toBeVisible();
});
test('shows error for entity with dependents', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityWithDependentsId}}');
await page.getByRole('button', { name: /delete/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /confirm|delete/i }).click();
await expect(page.getByRole('alert')).toContainText(/cannot delete/i);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Delete confirmed | Dialog confirmed → entity removed → list page |
| Delete from list | Row action → confirm → row removed |
| Cancel deletion | Dialog cancelled → entity intact |
| Dependent error | Entity with children → deletion blocked |
| Type-to-confirm | Confirm button disabled until name typed |
FILE:templates/crud/read.md
# Read Entity Template
Tests viewing entity details and list view with correct data display.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Seeded entity with ID `{{entityId}}` and name `{{entityName}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Read {{entityName}}', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
// Happy path: detail page
test('displays entity details correctly', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
await expect(page.getByRole('heading', { name: '{{expectedTitle}}' })).toBeVisible();
await expect(page.getByText('{{expectedField}}')).toBeVisible();
await expect(page.getByText('{{expectedCategory}}')).toBeVisible();
});
// Happy path: list view shows all items
test('displays list of entities', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s');
await expect(page.getByRole('table')).toBeVisible();
const rows = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
await expect(rows).toHaveCount({{expectedItemCount}});
});
// Happy path: list item links to detail
test('clicking list item navigates to detail page', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s');
await page.getByRole('link', { name: '{{expectedTitle}}' }).click();
await expect(page).toHaveURL(`{{baseUrl}}/{{entityName}}s/{{entityId}}`);
});
// Happy path: breadcrumb navigation
test('breadcrumb shows correct path', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
await expect(page.getByRole('navigation', { name: /breadcrumb/i })).toContainText('{{entityName}}s');
await expect(page.getByRole('navigation', { name: /breadcrumb/i })).toContainText('{{expectedTitle}}');
});
// Error case: non-existent entity shows 404
test('shows 404 for non-existent entity', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/999999');
await expect(page.getByRole('heading', { name: /404|not found/i })).toBeVisible();
});
// Edge case: loading state resolves to data
test('shows data after loading completes', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
// Skeleton/spinner should be gone, data visible
await expect(page.getByTestId('skeleton')).toBeHidden();
await expect(page.getByRole('heading', { name: '{{expectedTitle}}' })).toBeVisible();
});
// Edge case: empty list state
test('shows empty state when no entities exist', async ({ page }) => {
// Assumes a fresh context or filter that returns no results
await page.goto('{{baseUrl}}/{{entityName}}s?filter={{emptyFilter}}');
await expect(page.getByText(/no {{entityName}}s found/i)).toBeVisible();
await expect(page.getByRole('button', { name: /create|add/i })).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Read {{entityName}}', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('displays entity details correctly', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
await expect(page.getByRole('heading', { name: '{{expectedTitle}}' })).toBeVisible();
await expect(page.getByText('{{expectedField}}')).toBeVisible();
});
test('displays list of entities with correct count', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s');
const rows = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
await expect(rows).toHaveCount({{expectedItemCount}});
});
test('shows 404 for non-existent entity', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/999999');
await expect(page.getByRole('heading', { name: /404|not found/i })).toBeVisible();
});
test('shows empty state when list is empty', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s?filter={{emptyFilter}}');
await expect(page.getByText(/no {{entityName}}s found/i)).toBeVisible();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Detail view | Entity fields rendered correctly |
| List view | Correct row count in table |
| List → detail | Clicking row/link navigates correctly |
| Breadcrumb | Path reflects current location |
| 404 | Non-existent ID shows not-found page |
| Loading → data | Skeleton hidden, data visible after load |
| Empty list | No-results state with call to action |
FILE:templates/crud/soft-delete.md
# Soft Delete (Archive/Restore) Template
Tests archiving an entity, viewing archived items, and restoring them.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Active entity: ID `{{entityId}}`, name `{{entityName}}`
- Archived entity: ID `{{archivedEntityId}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Soft Delete — Archive & Restore', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
// Happy path: archive entity
test('archives entity and removes from active list', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
await page.getByRole('button', { name: /archive/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /archive|confirm/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/{{entityName}}s');
await expect(page.getByRole('alert')).toContainText(/archived/i);
await expect(page.getByRole('link', { name: '{{entityName}}' })).toBeHidden();
});
// Happy path: archived entity appears in archived view
test('archived entity visible in archived list', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s?status=archived');
await expect(page.getByRole('link', { name: '{{entityName}}' })).toBeVisible();
});
// Happy path: restore archived entity
test('restores archived entity to active list', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s?status=archived');
const row = page.getByRole('row', { name: new RegExp('{{entityName}}') });
await row.getByRole('button', { name: /restore/i }).click();
await expect(page.getByRole('alert')).toContainText(/restored/i);
await expect(row).toBeHidden();
await page.goto('{{baseUrl}}/{{entityName}}s');
await expect(page.getByRole('link', { name: '{{entityName}}' })).toBeVisible();
});
// Happy path: active list does not show archived by default
test('active list does not include archived entities', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s');
await expect(page.getByRole('link', { name: /{{archivedEntityName}}/i })).toBeHidden();
});
// Error case: archived entity cannot be edited
test('archived entity edit button is disabled', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{archivedEntityId}}');
await expect(page.getByRole('button', { name: /edit/i })).toBeDisabled();
await expect(page.getByText(/archived/i)).toBeVisible();
});
// Edge case: permanently delete archived entity
test('permanently deletes archived entity', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{archivedEntityId}}');
await page.getByRole('button', { name: /delete permanently/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /delete permanently/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/{{entityName}}s?status=archived');
await expect(page.getByRole('link', { name: '{{archivedEntityName}}' })).toBeHidden();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Soft Delete — Archive & Restore', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('archives entity and removes from active list', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
await page.getByRole('button', { name: /archive/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /archive|confirm/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/{{entityName}}s');
await expect(page.getByRole('link', { name: '{{entityName}}' })).toBeHidden();
});
test('restores archived entity to active list', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s?status=archived');
await page.getByRole('row', { name: new RegExp('{{entityName}}') })
.getByRole('button', { name: /restore/i }).click();
await expect(page.getByRole('alert')).toContainText(/restored/i);
});
test('archived entity edit button is disabled', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{archivedEntityId}}');
await expect(page.getByRole('button', { name: /edit/i })).toBeDisabled();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Archive | Entity moved to archived list, removed from active |
| Archived list | Archived items visible with status=archived filter |
| Restore | Archived entity returned to active list |
| Active list clean | Archived items hidden from default view |
| Edit disabled | Archived entity cannot be edited |
| Permanent delete | Hard-delete of archived entity |
FILE:templates/crud/update.md
# Update Entity Template
Tests editing an entity via form and inline edit interactions.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Existing entity ID: `{{entityId}}`, name: `{{originalEntityName}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Update {{entityName}}', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
// Happy path: edit via form
test('updates entity via edit form', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}/edit');
const nameField = page.getByRole('textbox', { name: /name/i });
await nameField.clear();
await nameField.fill('{{updatedEntityName}}');
await page.getByRole('button', { name: /save|update/i }).click();
await expect(page).toHaveURL(`{{baseUrl}}/{{entityName}}s/{{entityId}}`);
await expect(page.getByRole('heading', { name: '{{updatedEntityName}}' })).toBeVisible();
await expect(page.getByRole('alert')).toContainText(/updated successfully/i);
});
// Happy path: inline edit
test('updates name via inline edit', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
await page.getByRole('button', { name: /edit name/i }).click();
const inlineInput = page.getByRole('textbox', { name: /name/i });
await inlineInput.clear();
await inlineInput.fill('{{updatedEntityName}}');
await inlineInput.press('Enter');
await expect(page.getByRole('heading', { name: '{{updatedEntityName}}' })).toBeVisible();
});
// Happy path: edit then navigate away — unsaved changes warning
test('warns before discarding unsaved changes', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}/edit');
await page.getByRole('textbox', { name: /name/i }).fill('unsaved-change');
await page.getByRole('link', { name: /cancel|back/i }).click();
await expect(page.getByRole('dialog', { name: /unsaved changes/i })).toBeVisible();
await page.getByRole('button', { name: /discard/i }).click();
await expect(page).toHaveURL(`{{baseUrl}}/{{entityName}}s/{{entityId}}`);
await expect(page.getByRole('heading', { name: '{{originalEntityName}}' })).toBeVisible();
});
// Error case: clearing required field
test('shows validation error when required field is cleared', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}/edit');
await page.getByRole('textbox', { name: /name/i }).clear();
await page.getByRole('button', { name: /save|update/i }).click();
await expect(page.getByText(/name is required/i)).toBeVisible();
await expect(page).toHaveURL(`{{baseUrl}}/{{entityName}}s/{{entityId}}/edit`);
});
// Error case: conflict (optimistic update failure)
test('handles concurrent edit conflict gracefully', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}/edit');
// Simulate another user modifying the record
await page.request.put(`{{baseUrl}}/api/{{entityName}}s/{{entityId}}`, {
data: { name: 'modified-by-other', version: 999 },
});
await page.getByRole('textbox', { name: /name/i }).fill('my-change');
await page.getByRole('button', { name: /save|update/i }).click();
await expect(page.getByRole('alert')).toContainText(/conflict|modified by another/i);
});
// Edge case: inline edit cancelled with Escape
test('cancels inline edit on Escape key', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
await page.getByRole('button', { name: /edit name/i }).click();
await page.getByRole('textbox', { name: /name/i }).fill('should-not-save');
await page.keyboard.press('Escape');
await expect(page.getByRole('heading', { name: '{{originalEntityName}}' })).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Update {{entityName}}', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('updates entity via edit form', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}/edit');
await page.getByRole('textbox', { name: /name/i }).clear();
await page.getByRole('textbox', { name: /name/i }).fill('{{updatedEntityName}}');
await page.getByRole('button', { name: /save|update/i }).click();
await expect(page.getByRole('heading', { name: '{{updatedEntityName}}' })).toBeVisible();
});
test('shows validation error when required field cleared', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}/edit');
await page.getByRole('textbox', { name: /name/i }).clear();
await page.getByRole('button', { name: /save|update/i }).click();
await expect(page.getByText(/name is required/i)).toBeVisible();
});
test('cancels inline edit on Escape', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s/{{entityId}}');
await page.getByRole('button', { name: /edit name/i }).click();
await page.getByRole('textbox', { name: /name/i }).fill('nope');
await page.keyboard.press('Escape');
await expect(page.getByRole('heading', { name: '{{originalEntityName}}' })).toBeVisible();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Edit form | Full edit form → save → detail page |
| Inline edit | Click field → type → Enter to save |
| Unsaved changes | Navigation shows discard confirmation |
| Required field | Cleared required field → validation |
| Conflict | Concurrent edit → conflict error |
| Escape cancel | Inline edit cancelled, original value restored |
FILE:templates/dashboard/chart-rendering.md
# Chart Rendering Template
Tests chart visibility, interactive tooltips, and legend behaviour.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Dashboard with charts at `{{baseUrl}}/dashboard`
- Chart library: `{{chartLibrary}}` (e.g. Chart.js, Recharts, D3)
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Chart Rendering', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
// Wait for chart container to be visible
await expect(page.getByRole('img', { name: /{{chartName}} chart/i })
.or(page.getByTestId('{{chartTestId}}'))).toBeVisible();
});
// Happy path: chart rendered and visible
test('renders {{chartName}} chart', async ({ page }) => {
const chart = page.getByTestId('{{chartTestId}}');
await expect(chart).toBeVisible();
// Chart has non-zero dimensions
const box = await chart.boundingBox();
expect(box?.width).toBeGreaterThan(0);
expect(box?.height).toBeGreaterThan(0);
});
// Happy path: tooltip shown on hover
test('shows tooltip on data point hover', async ({ page }) => {
const chart = page.getByTestId('{{chartTestId}}');
const box = await chart.boundingBox();
// Hover over the centre of the chart
await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2);
await expect(page.getByRole('tooltip')).toBeVisible();
await expect(page.getByRole('tooltip')).toContainText(/\d/);
});
// Happy path: legend visible with correct labels
test('displays chart legend with correct series labels', async ({ page }) => {
const legend = page.getByRole('list', { name: /legend/i });
await expect(legend).toBeVisible();
await expect(legend.getByRole('listitem', { name: '{{seriesName1}}' })).toBeVisible();
await expect(legend.getByRole('listitem', { name: '{{seriesName2}}' })).toBeVisible();
});
// Happy path: clicking legend toggles series visibility
test('toggles series visibility via legend click', async ({ page }) => {
await page.getByRole('button', { name: '{{seriesName1}}' }).click();
// Series hidden — legend item shows struck-through or disabled state
await expect(page.getByRole('button', { name: '{{seriesName1}}' })).toHaveAttribute('aria-pressed', 'false');
});
// Happy path: chart updates when date range changed
test('updates chart when date range filter applied', async ({ page }) => {
const before = await page.getByTestId('{{chartTestId}}').screenshot();
await page.getByRole('combobox', { name: /date range/i }).selectOption('last_7_days');
const after = await page.getByTestId('{{chartTestId}}').screenshot();
expect(Buffer.compare(before, after)).not.toBe(0);
});
// Error case: empty data shows no-data state
test('shows no-data message when chart has no data', async ({ page }) => {
await page.route('{{baseUrl}}/api/chart-data*', route =>
route.fulfill({ status: 200, body: JSON.stringify({ data: [] }) })
);
await page.reload();
const chart = page.getByTestId('{{chartTestId}}');
await expect(chart.getByText(/no data|no results/i)).toBeVisible();
});
// Edge case: chart accessible via aria
test('chart has accessible title and description', async ({ page }) => {
const chart = page.getByTestId('{{chartTestId}}');
await expect(chart.getByRole('img')).toHaveAttribute('aria-label', /{{chartName}}/i);
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Chart Rendering', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('renders chart with non-zero dimensions', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
const chart = page.getByTestId('{{chartTestId}}');
await expect(chart).toBeVisible();
const box = await chart.boundingBox();
expect(box?.width).toBeGreaterThan(0);
expect(box?.height).toBeGreaterThan(0);
});
test('shows tooltip on hover', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
const chart = page.getByTestId('{{chartTestId}}');
const box = await chart.boundingBox();
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await expect(page.getByRole('tooltip')).toBeVisible();
});
test('displays legend labels', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByRole('list', { name: /legend/i })).toBeVisible();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Chart visible | Non-zero bounding box confirmed |
| Tooltip on hover | Tooltip appears with numeric value |
| Legend labels | Series names present in legend |
| Legend toggle | Click hides/shows series |
| Date range update | Chart changes when filter applied |
| No-data state | Empty dataset → no-data message |
| Accessible label | aria-label present on chart element |
FILE:templates/dashboard/data-loading.md
# Dashboard Data Loading Template
Tests loading state, skeleton screens, and data display after fetch.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Dashboard at `{{baseUrl}}/dashboard`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Dashboard Data Loading', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
// Happy path: skeleton shown then replaced by data
test('shows skeleton during load, then displays data', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
// Skeleton should resolve; real data appears
await expect(page.getByTestId('skeleton')).toBeHidden();
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
await expect(page.getByRole('region', { name: /{{widgetName}}/i })).toBeVisible();
});
// Happy path: all metric cards populated
test('renders metric cards with values', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
const cards = page.getByRole('article', { name: /metric/i });
await expect(cards).toHaveCount({{expectedMetricCount}});
await expect(cards.first().getByText(/\d/)).toBeVisible();
});
// Happy path: data updates on refresh
test('refreshes data when refresh button clicked', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByTestId('skeleton')).toBeHidden();
const before = await page.getByTestId('{{metricId}}').textContent();
await page.getByRole('button', { name: /refresh/i }).click();
await expect(page.getByTestId('skeleton')).toBeHidden();
// Value may or may not change — just confirm data loads again
await expect(page.getByTestId('{{metricId}}')).toBeVisible();
});
// Error case: shows error state when API fails
test('shows error state when data fetch fails', async ({ page }) => {
await page.route('{{baseUrl}}/api/dashboard*', route =>
route.fulfill({ status: 500, body: JSON.stringify({ error: 'Internal Server Error' }) })
);
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByRole('alert')).toContainText(/failed to load|error loading/i);
await expect(page.getByRole('button', { name: /retry/i })).toBeVisible();
});
// Error case: retry after failure loads data
test('retries and loads data after error', async ({ page }) => {
let callCount = 0;
await page.route('{{baseUrl}}/api/dashboard*', route => {
callCount++;
if (callCount === 1) return route.fulfill({ status: 500, body: '{}' });
return route.continue();
});
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /retry/i }).click();
await expect(page.getByTestId('skeleton')).toBeHidden();
await expect(page.getByRole('region', { name: /{{widgetName}}/i })).toBeVisible();
});
// Edge case: slow network shows skeleton for duration
test('skeleton persists during slow API response', async ({ page }) => {
await page.route('{{baseUrl}}/api/dashboard*', async route => {
await new Promise(r => setTimeout(r, 2000));
await route.continue();
});
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByTestId('skeleton')).toBeVisible();
await expect(page.getByTestId('skeleton')).toBeHidden(); // eventually resolves
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Dashboard Data Loading', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('renders metric cards after loading', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByTestId('skeleton')).toBeHidden();
await expect(page.getByRole('article', { name: /metric/i }).first()).toBeVisible();
});
test('shows error state on API failure', async ({ page }) => {
await page.route('{{baseUrl}}/api/dashboard*', route =>
route.fulfill({ status: 500, body: '{}' })
);
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByRole('alert')).toContainText(/failed to load|error/i);
await expect(page.getByRole('button', { name: /retry/i })).toBeVisible();
});
test('skeleton visible during slow response', async ({ page }) => {
await page.route('{{baseUrl}}/api/dashboard*', async route => {
await new Promise(r => setTimeout(r, 1500));
await route.continue();
});
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByTestId('skeleton')).toBeVisible();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Skeleton → data | Loading state resolves to populated widgets |
| Metric cards | N cards each showing a numeric value |
| Refresh | Data reloaded on button click |
| API error | Error alert + retry button shown |
| Retry success | Second request succeeds after failure |
| Slow network | Skeleton persists during delay |
FILE:templates/dashboard/date-range-filter.md
# Date Range Filter Template
Tests date picker interaction, preset ranges, and data refresh on selection.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Dashboard at `{{baseUrl}}/dashboard`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Date Range Filter', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
});
// Happy path: preset range — last 7 days
test('applies "last 7 days" preset', async ({ page }) => {
await page.getByRole('button', { name: /date range/i }).click();
await page.getByRole('option', { name: /last 7 days/i }).click();
await expect(page).toHaveURL(/from=|start_date=/);
await expect(page.getByRole('button', { name: /date range/i })).toContainText(/last 7 days/i);
});
// Happy path: preset range — last 30 days
test('applies "last 30 days" preset', async ({ page }) => {
await page.getByRole('button', { name: /date range/i }).click();
await page.getByRole('option', { name: /last 30 days/i }).click();
await expect(page.getByRole('button', { name: /date range/i })).toContainText(/last 30 days/i);
await expect(page.getByTestId('skeleton')).toBeHidden();
});
// Happy path: custom date range via date picker
test('applies custom date range from picker', async ({ page }) => {
await page.getByRole('button', { name: /date range/i }).click();
await page.getByRole('option', { name: /custom/i }).click();
const picker = page.getByRole('dialog', { name: /date range/i });
await expect(picker).toBeVisible();
// Select start date
await picker.getByRole('button', { name: '{{startDay}}' }).click();
// Select end date
await picker.getByRole('button', { name: '{{endDay}}' }).click();
await picker.getByRole('button', { name: /apply/i }).click();
await expect(picker).toBeHidden();
await expect(page.getByRole('button', { name: /date range/i })).toContainText('{{startDateFormatted}}');
});
// Happy path: data reloads on range change
test('reloads dashboard data on date range change', async ({ page }) => {
let requestCount = 0;
await page.route('{{baseUrl}}/api/dashboard*', route => {
requestCount++;
return route.continue();
});
await page.getByRole('button', { name: /date range/i }).click();
await page.getByRole('option', { name: /last 7 days/i }).click();
expect(requestCount).toBeGreaterThan(0);
await expect(page.getByTestId('skeleton')).toBeHidden();
});
// Error case: invalid custom range (end before start)
test('shows error when end date is before start date', async ({ page }) => {
await page.getByRole('button', { name: /date range/i }).click();
await page.getByRole('option', { name: /custom/i }).click();
const picker = page.getByRole('dialog', { name: /date range/i });
await picker.getByRole('button', { name: '{{endDay}}' }).click(); // pick later date first
await picker.getByRole('button', { name: '{{startDay}}' }).click(); // then earlier
await expect(picker.getByText(/end.*after.*start|invalid.*range/i)).toBeVisible();
await expect(picker.getByRole('button', { name: /apply/i })).toBeDisabled();
});
// Edge case: range persists after page reload
test('date range persists in URL after reload', async ({ page }) => {
await page.getByRole('button', { name: /date range/i }).click();
await page.getByRole('option', { name: /last 7 days/i }).click();
const url = page.url();
await page.reload();
await expect(page).toHaveURL(url);
await expect(page.getByRole('button', { name: /date range/i })).toContainText(/last 7 days/i);
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Date Range Filter', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('applies last-7-days preset', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /date range/i }).click();
await page.getByRole('option', { name: /last 7 days/i }).click();
await expect(page.getByRole('button', { name: /date range/i })).toContainText(/last 7 days/i);
});
test('shows error for invalid range', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /date range/i }).click();
await page.getByRole('option', { name: /custom/i }).click();
const picker = page.getByRole('dialog', { name: /date range/i });
await picker.getByRole('button', { name: '{{endDay}}' }).click();
await picker.getByRole('button', { name: '{{startDay}}' }).click();
await expect(picker.getByRole('button', { name: /apply/i })).toBeDisabled();
});
test('range persists after page reload', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /date range/i }).click();
await page.getByRole('option', { name: /last 7 days/i }).click();
const url = page.url();
await page.reload();
await expect(page).toHaveURL(url);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Last 7 days | Preset applied, URL updated |
| Last 30 days | Preset applied, data refreshed |
| Custom range | Date picker → start + end → apply |
| Data reload | API called again on range change |
| Invalid range | End before start → apply disabled |
| URL persistence | Range in URL survives reload |
FILE:templates/dashboard/export.md
# Export Template
Tests CSV and PDF export, download triggering, and file verification.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Dashboard or report page at `{{baseUrl}}/{{reportPath}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
import path from 'path';
import fs from 'fs';
test.describe('Export', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{reportPath}}');
});
// Happy path: CSV download
test('downloads CSV export', async ({ page }) => {
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: /export.*csv|download.*csv/i }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.csv$/);
const filePath = path.join('/tmp', download.suggestedFilename());
await download.saveAs(filePath);
const content = fs.readFileSync(filePath, 'utf-8');
expect(content).toContain('{{expectedCsvHeader}}');
expect(content.split('\n').length).toBeGreaterThan(1);
});
// Happy path: PDF download
test('downloads PDF export', async ({ page }) => {
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: /export.*pdf|download.*pdf/i }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.pdf$/);
const filePath = path.join('/tmp', download.suggestedFilename());
await download.saveAs(filePath);
const buffer = fs.readFileSync(filePath);
// PDF magic bytes
expect(buffer.slice(0, 4).toString()).toBe('%PDF');
});
// Happy path: export with current filters applied
test('export respects active date range filter', async ({ page }) => {
await page.getByRole('button', { name: /date range/i }).click();
await page.getByRole('option', { name: /last 7 days/i }).click();
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: /export.*csv/i }).click();
const download = await downloadPromise;
const filePath = path.join('/tmp', download.suggestedFilename());
await download.saveAs(filePath);
const content = fs.readFileSync(filePath, 'utf-8');
expect(content.split('\n').length).toBeGreaterThan(1);
});
// Happy path: export loading indicator
test('shows loading state during export generation', async ({ page }) => {
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: /export.*csv/i }).click();
await expect(page.getByRole('button', { name: /export.*csv/i })).toBeDisabled();
await downloadPromise;
await expect(page.getByRole('button', { name: /export.*csv/i })).toBeEnabled();
});
// Error case: export fails with server error
test('shows error when export generation fails', async ({ page }) => {
await page.route('{{baseUrl}}/api/export*', route =>
route.fulfill({ status: 500, body: JSON.stringify({ error: 'Export failed' }) })
);
await page.getByRole('button', { name: /export.*csv/i }).click();
await expect(page.getByRole('alert')).toContainText(/export failed|could not generate/i);
});
// Edge case: export with no data shows warning
test('shows warning when exporting empty dataset', async ({ page }) => {
await page.route('{{baseUrl}}/api/{{reportEndpoint}}*', route =>
route.fulfill({ status: 200, body: JSON.stringify({ data: [] }) })
);
await page.reload();
await page.getByRole('button', { name: /export.*csv/i }).click();
await expect(page.getByRole('alert')).toContainText(/no data to export|empty/i);
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
const path = require('path');
const fs = require('fs');
test.describe('Export', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('downloads CSV export with correct header', async ({ page }) => {
await page.goto('{{baseUrl}}/{{reportPath}}');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: /export.*csv/i }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.csv$/);
const filePath = path.join('/tmp', download.suggestedFilename());
await download.saveAs(filePath);
expect(fs.readFileSync(filePath, 'utf-8')).toContain('{{expectedCsvHeader}}');
});
test('downloads PDF with correct magic bytes', async ({ page }) => {
await page.goto('{{baseUrl}}/{{reportPath}}');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: /export.*pdf/i }).click();
const download = await downloadPromise;
const filePath = path.join('/tmp', download.suggestedFilename());
await download.saveAs(filePath);
expect(fs.readFileSync(filePath).slice(0, 4).toString()).toBe('%PDF');
});
test('shows error when export fails', async ({ page }) => {
await page.goto('{{baseUrl}}/{{reportPath}}');
await page.route('{{baseUrl}}/api/export*', route =>
route.fulfill({ status: 500, body: '{}' })
);
await page.getByRole('button', { name: /export.*csv/i }).click();
await expect(page.getByRole('alert')).toContainText(/export failed/i);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| CSV download | File downloaded, header row verified |
| PDF download | File downloaded, %PDF magic bytes checked |
| Filtered export | Active filters applied to exported data |
| Loading state | Button disabled during generation |
| Server error | Export failure → error alert |
| Empty dataset | No-data warning shown |
FILE:templates/dashboard/realtime-updates.md
# Realtime Updates Template
Tests live data via WebSocket or polling, connection handling, and reconnection.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Dashboard with live data at `{{baseUrl}}/dashboard`
- WebSocket endpoint: `{{wsEndpoint}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Realtime Updates', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
// Happy path: live metric updates via WebSocket
test('updates metric when WebSocket message received', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByTestId('{{metricId}}')).toBeVisible();
// Inject a WS message to simulate server push
await page.evaluate(() => {
const ws = (window as any).__dashboardWs;
if (ws) ws.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({ type: 'metric_update', id: '{{metricId}}', value: 9999 })
}));
});
await expect(page.getByTestId('{{metricId}}')).toContainText('9,999');
});
// Happy path: connection status indicator
test('shows "connected" status indicator', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByRole('status', { name: /live|connected/i })).toBeVisible();
});
// Happy path: data highlighted on update
test('highlights updated value briefly', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.evaluate(() => {
const ws = (window as any).__dashboardWs;
if (ws) ws.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({ type: 'metric_update', id: '{{metricId}}', value: 42 })
}));
});
await expect(page.getByTestId('{{metricId}}')).toHaveClass(/updated|flash/);
// Highlight fades
await expect(page.getByTestId('{{metricId}}')).not.toHaveClass(/updated|flash/);
});
// Error case: WebSocket disconnected — shows offline indicator
test('shows disconnected state when WebSocket closes', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.evaluate(() => {
const ws = (window as any).__dashboardWs;
if (ws) ws.close();
});
await expect(page.getByRole('status', { name: /disconnected|offline/i })).toBeVisible();
await expect(page.getByText(/reconnecting/i)).toBeVisible();
});
// Error case: connection refused — error state shown
test('shows connection error when WebSocket cannot connect', async ({ page }) => {
await page.route('**/{{wsEndpoint}}', route => route.abort());
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByRole('alert')).toContainText(/connection.*failed|live updates.*unavailable/i);
});
// Edge case: reconnects automatically after disconnect
test('reconnects automatically after network interruption', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.evaluate(() => {
const ws = (window as any).__dashboardWs;
if (ws) ws.close();
});
await expect(page.getByRole('status', { name: /disconnected/i })).toBeVisible();
// Wait for auto-reconnect
await expect(page.getByRole('status', { name: /connected|live/i })).toBeVisible();
});
// Edge case: stale data badge shown when disconnected
test('shows stale data warning when disconnected', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.evaluate(() => {
const ws = (window as any).__dashboardWs;
if (ws) ws.close();
});
await expect(page.getByText(/data may be outdated|stale/i)).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Realtime Updates', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('shows connected status on load', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByRole('status', { name: /live|connected/i })).toBeVisible();
});
test('shows disconnected state when WS closes', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.evaluate(() => {
const ws = window.__dashboardWs;
if (ws) ws.close();
});
await expect(page.getByRole('status', { name: /disconnected|offline/i })).toBeVisible();
});
test('updates metric on WS message', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.evaluate(() => {
const ws = window.__dashboardWs;
if (ws) ws.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({ type: 'metric_update', id: '{{metricId}}', value: 9999 })
}));
});
await expect(page.getByTestId('{{metricId}}')).toContainText('9,999');
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Live update | WS message updates metric value |
| Connected status | Status indicator shows "live" |
| Update highlight | Changed value briefly highlighted |
| Disconnected | WS close → disconnected indicator |
| Connection refused | WS blocked → error alert |
| Auto-reconnect | Reconnects after close |
| Stale data | Warning shown while disconnected |
FILE:templates/forms/autosave.md
# Autosave Template
Tests auto-save draft functionality and draft restoration on revisit.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Form with autosave at `{{baseUrl}}/{{formPath}}`
- Autosave interval: `{{autosaveIntervalMs}}` ms
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Autosave', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
});
// Happy path: autosave indicator appears after typing
test('shows autosave indicator after typing', async ({ page }) => {
await page.getByRole('textbox', { name: /{{fieldLabel}}/i }).fill('{{draftContent}}');
await page.clock.install();
await page.clock.fastForward({{autosaveIntervalMs}});
await expect(page.getByText(/saved|draft saved/i)).toBeVisible();
});
// Happy path: draft restored on revisit
test('restores draft on page revisit', async ({ page }) => {
await page.getByRole('textbox', { name: /{{fieldLabel}}/i }).fill('{{draftContent}}');
await page.clock.install();
await page.clock.fastForward({{autosaveIntervalMs}});
await expect(page.getByText(/draft saved/i)).toBeVisible();
// Simulate revisit
await page.reload();
await expect(page.getByRole('textbox', { name: /{{fieldLabel}}/i })).toHaveValue('{{draftContent}}');
await expect(page.getByText(/draft restored|you have a saved draft/i)).toBeVisible();
});
// Happy path: restore draft via banner
test('restores draft when user clicks restore', async ({ page }) => {
await page.reload();
const banner = page.getByRole('alert', { name: /saved draft/i });
if (await banner.isVisible()) {
await banner.getByRole('button', { name: /restore/i }).click();
await expect(page.getByRole('textbox', { name: /{{fieldLabel}}/i })).toHaveValue('{{draftContent}}');
}
});
// Happy path: dismiss draft banner discards old draft
test('discards draft when user clicks dismiss', async ({ page }) => {
await page.reload();
const banner = page.getByRole('alert', { name: /saved draft/i });
if (await banner.isVisible()) {
await banner.getByRole('button', { name: /dismiss|discard/i }).click();
await expect(banner).toBeHidden();
await expect(page.getByRole('textbox', { name: /{{fieldLabel}}/i })).toHaveValue('');
}
});
// Happy path: draft cleared after successful submit
test('clears autosaved draft after form submission', async ({ page }) => {
await page.getByRole('textbox', { name: /{{fieldLabel}}/i }).fill('{{draftContent}}');
await page.clock.install();
await page.clock.fastForward({{autosaveIntervalMs}});
await page.getByRole('button', { name: /submit|save/i }).click();
await expect(page.getByRole('alert')).toContainText(/submitted|saved/i);
// Revisit — no draft banner
await page.goto('{{baseUrl}}/{{formPath}}');
await expect(page.getByRole('alert', { name: /saved draft/i })).toBeHidden();
});
// Error case: autosave fails silently and retries
test('shows autosave error when network fails', async ({ page }) => {
await page.route('{{baseUrl}}/api/drafts*', route => route.abort('failed'));
await page.getByRole('textbox', { name: /{{fieldLabel}}/i }).fill('{{draftContent}}');
await page.clock.install();
await page.clock.fastForward({{autosaveIntervalMs}});
await expect(page.getByText(/save failed|could not save/i)).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Autosave', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('shows autosave indicator after interval', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
await page.getByRole('textbox', { name: /{{fieldLabel}}/i }).fill('{{draftContent}}');
await page.clock.install();
await page.clock.fastForward({{autosaveIntervalMs}});
await expect(page.getByText(/draft saved/i)).toBeVisible();
});
test('restores draft on page revisit', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
await page.getByRole('textbox', { name: /{{fieldLabel}}/i }).fill('{{draftContent}}');
await page.clock.install();
await page.clock.fastForward({{autosaveIntervalMs}});
await page.reload();
await expect(page.getByRole('textbox', { name: /{{fieldLabel}}/i })).toHaveValue('{{draftContent}}');
});
test('clears draft after successful submit', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
await page.getByRole('textbox', { name: /{{fieldLabel}}/i }).fill('{{draftContent}}');
await page.clock.install();
await page.clock.fastForward({{autosaveIntervalMs}});
await page.getByRole('button', { name: /submit/i }).click();
await page.goto('{{baseUrl}}/{{formPath}}');
await expect(page.getByRole('alert', { name: /saved draft/i })).toBeHidden();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Autosave indicator | "Draft saved" shown after interval |
| Draft restored | Revisit → field pre-filled |
| Restore via banner | Banner restore button populates field |
| Dismiss draft | Discard clears saved value |
| Cleared on submit | No draft banner after successful submit |
| Network failure | Save-failed message shown |
FILE:templates/forms/conditional-fields.md
# Conditional Fields Template
Tests show/hide fields based on selection and correct validation of visible fields only.
## Prerequisites
- Form at `{{baseUrl}}/{{formPath}}`
- Trigger field: `{{triggerField}}` (e.g. country, type selector)
- Conditional field shown when value is `{{triggerValue}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Conditional Fields', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
});
// Happy path: conditional field shown on trigger
test('shows conditional field when trigger value selected', async ({ page }) => {
await expect(page.getByRole('textbox', { name: /{{conditionalFieldLabel}}/i })).toBeHidden();
await page.getByRole('combobox', { name: /{{triggerField}}/i }).selectOption('{{triggerValue}}');
await expect(page.getByRole('textbox', { name: /{{conditionalFieldLabel}}/i })).toBeVisible();
});
// Happy path: conditional field hidden when trigger changes
test('hides conditional field when trigger changes back', async ({ page }) => {
await page.getByRole('combobox', { name: /{{triggerField}}/i }).selectOption('{{triggerValue}}');
await expect(page.getByRole('textbox', { name: /{{conditionalFieldLabel}}/i })).toBeVisible();
await page.getByRole('combobox', { name: /{{triggerField}}/i }).selectOption('{{nonTriggerValue}}');
await expect(page.getByRole('textbox', { name: /{{conditionalFieldLabel}}/i })).toBeHidden();
});
// Happy path: form submits with conditional field filled
test('submits form when conditional field is shown and filled', async ({ page }) => {
await page.getByRole('combobox', { name: /{{triggerField}}/i }).selectOption('{{triggerValue}}');
await page.getByRole('textbox', { name: /{{conditionalFieldLabel}}/i }).fill('{{conditionalFieldValue}}');
await page.getByRole('button', { name: /submit/i }).click();
await expect(page.getByRole('alert')).toContainText(/submitted|saved/i);
});
// Error case: conditional field required when visible
test('validates conditional field when it is visible', async ({ page }) => {
await page.getByRole('combobox', { name: /{{triggerField}}/i }).selectOption('{{triggerValue}}');
await page.getByRole('button', { name: /submit/i }).click();
await expect(page.getByText(/{{conditionalFieldLabel}}.*required/i)).toBeVisible();
});
// Error case: hidden field not validated
test('does not validate conditional field when hidden', async ({ page }) => {
await page.getByRole('combobox', { name: /{{triggerField}}/i }).selectOption('{{nonTriggerValue}}');
await page.getByRole('button', { name: /submit/i }).click();
await expect(page.getByText(/{{conditionalFieldLabel}}.*required/i)).toBeHidden();
});
// Edge case: conditional field value cleared when hidden
test('clears conditional field value when field is hidden', async ({ page }) => {
await page.getByRole('combobox', { name: /{{triggerField}}/i }).selectOption('{{triggerValue}}');
await page.getByRole('textbox', { name: /{{conditionalFieldLabel}}/i }).fill('some value');
await page.getByRole('combobox', { name: /{{triggerField}}/i }).selectOption('{{nonTriggerValue}}');
// Re-show and verify value is cleared
await page.getByRole('combobox', { name: /{{triggerField}}/i }).selectOption('{{triggerValue}}');
await expect(page.getByRole('textbox', { name: /{{conditionalFieldLabel}}/i })).toHaveValue('');
});
// Edge case: radio trigger shows/hides field
test('shows field based on radio button selection', async ({ page }) => {
await page.getByRole('radio', { name: '{{radioTriggerLabel}}' }).check();
await expect(page.getByRole('textbox', { name: /{{conditionalFieldLabel}}/i })).toBeVisible();
await page.getByRole('radio', { name: '{{radioOtherLabel}}' }).check();
await expect(page.getByRole('textbox', { name: /{{conditionalFieldLabel}}/i })).toBeHidden();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Conditional Fields', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
});
test('shows conditional field when trigger selected', async ({ page }) => {
await expect(page.getByRole('textbox', { name: /{{conditionalFieldLabel}}/i })).toBeHidden();
await page.getByRole('combobox', { name: /{{triggerField}}/i }).selectOption('{{triggerValue}}');
await expect(page.getByRole('textbox', { name: /{{conditionalFieldLabel}}/i })).toBeVisible();
});
test('validates visible conditional field on submit', async ({ page }) => {
await page.getByRole('combobox', { name: /{{triggerField}}/i }).selectOption('{{triggerValue}}');
await page.getByRole('button', { name: /submit/i }).click();
await expect(page.getByText(/{{conditionalFieldLabel}}.*required/i)).toBeVisible();
});
test('does not validate hidden conditional field', async ({ page }) => {
await page.getByRole('combobox', { name: /{{triggerField}}/i }).selectOption('{{nonTriggerValue}}');
await page.getByRole('button', { name: /submit/i }).click();
await expect(page.getByText(/{{conditionalFieldLabel}}.*required/i)).toBeHidden();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Show on trigger | Selecting value reveals hidden field |
| Hide on change | Changing back hides field again |
| Submit with field | Visible field filled → success |
| Validate visible | Visible empty field → required error |
| Skip hidden | Hidden field not validated |
| Clear on hide | Value cleared when field hidden |
| Radio trigger | Radio button controls field visibility |
FILE:templates/forms/file-upload.md
# File Upload Template
Tests single file, multiple files, drag-and-drop, and upload progress.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Test files available: `{{testFilePath}}`, `{{largeFilePath}}`
- Accepted types: `{{acceptedMimeTypes}}` (e.g. image/jpeg, application/pdf)
- Max file size: `{{maxFileSizeMb}}` MB
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
import path from 'path';
const testFile = path.resolve('{{testFilePath}}');
const largeFile = path.resolve('{{largeFilePath}}');
test.describe('File Upload', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{uploadPath}}');
});
// Happy path: single file upload
test('uploads a single file successfully', async ({ page }) => {
await page.getByRole('button', { name: /choose file|browse/i }).setInputFiles(testFile);
await expect(page.getByText(/{{testFileName}}/)).toBeVisible();
await page.getByRole('button', { name: /upload/i }).click();
await expect(page.getByRole('progressbar')).toBeVisible();
await expect(page.getByRole('alert')).toContainText(/upload.*complete|uploaded successfully/i);
});
// Happy path: multiple files
test('uploads multiple files', async ({ page }) => {
const input = page.getByRole('button', { name: /choose file|browse/i });
await input.setInputFiles([testFile, testFile]);
await expect(page.getByText(/2 files?|{{testFileName}}/i)).toBeVisible();
await page.getByRole('button', { name: /upload/i }).click();
await expect(page.getByRole('alert')).toContainText(/2.*uploaded/i);
});
// Happy path: drag and drop
test('uploads file via drag and drop', async ({ page }) => {
const dropzone = page.getByRole('region', { name: /drop.*files|drag.*here/i });
await expect(dropzone).toBeVisible();
// Use DataTransfer to simulate drag-drop
const dataTransfer = await page.evaluateHandle(() => new DataTransfer());
await dropzone.dispatchEvent('drop', { dataTransfer });
// Alternatively, use setInputFiles on the hidden input if dropzone wraps one
await page.locator('input[type="file"]').setInputFiles(testFile);
await expect(page.getByText(/{{testFileName}}/)).toBeVisible();
});
// Happy path: remove file from queue before upload
test('removes file from queue', async ({ page }) => {
await page.locator('input[type="file"]').setInputFiles(testFile);
await page.getByRole('button', { name: /remove.*{{testFileName}}|×/i }).click();
await expect(page.getByText(/{{testFileName}}/)).toBeHidden();
});
// Error case: file too large
test('shows error for oversized file', async ({ page }) => {
await page.locator('input[type="file"]').setInputFiles(largeFile);
await expect(page.getByText(/too large|exceeds.*{{maxFileSizeMb}}|max.*size/i)).toBeVisible();
});
// Error case: wrong file type
test('shows error for unsupported file type', async ({ page }) => {
const wrongTypeFile = { name: 'test.exe', mimeType: 'application/octet-stream', buffer: Buffer.from('data') };
await page.locator('input[type="file"]').setInputFiles(wrongTypeFile);
await expect(page.getByText(/unsupported.*type|{{acceptedMimeTypes}}.*only/i)).toBeVisible();
});
// Edge case: upload progress shown and completed
test('shows progress bar during upload', async ({ page }) => {
await page.locator('input[type="file"]').setInputFiles(testFile);
await page.getByRole('button', { name: /upload/i }).click();
const progress = page.getByRole('progressbar');
await expect(progress).toBeVisible();
await expect(progress).toBeHidden(); // completes and hides
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
const path = require('path');
test.describe('File Upload', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{uploadPath}}');
});
test('uploads single file', async ({ page }) => {
await page.locator('input[type="file"]').setInputFiles('{{testFilePath}}');
await page.getByRole('button', { name: /upload/i }).click();
await expect(page.getByRole('alert')).toContainText(/uploaded successfully/i);
});
test('shows error for oversized file', async ({ page }) => {
await page.locator('input[type="file"]').setInputFiles('{{largeFilePath}}');
await expect(page.getByText(/too large|exceeds/i)).toBeVisible();
});
test('shows error for wrong file type', async ({ page }) => {
await page.locator('input[type="file"]').setInputFiles({
name: 'bad.exe',
mimeType: 'application/octet-stream',
buffer: Buffer.from('x'),
});
await expect(page.getByText(/unsupported.*type/i)).toBeVisible();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Single file | File picker → upload → success |
| Multiple files | Two files queued and uploaded |
| Drag-and-drop | Drop event populates queue |
| Remove from queue | File removed before upload |
| Oversized | Error shown, upload blocked |
| Wrong type | Mime-type error shown |
| Progress bar | Progressbar visible during upload |
FILE:templates/forms/multi-step.md
# Multi-Step Form (Wizard) Template
Tests wizard step navigation, validation per step, and final submission.
## Prerequisites
- Form wizard at `{{baseUrl}}/{{wizardPath}}`
- Steps: Step 1 (personal), Step 2 (details), Step 3 (review)
---
## TypeScript
```typescript
import { test, expect, Page } from '@playwright/test';
async function completeStep1(page: Page): Promise<void> {
await page.getByRole('textbox', { name: /first name/i }).fill('{{firstName}}');
await page.getByRole('textbox', { name: /last name/i }).fill('{{lastName}}');
await page.getByRole('textbox', { name: /email/i }).fill('{{email}}');
await page.getByRole('button', { name: /next/i }).click();
}
async function completeStep2(page: Page): Promise<void> {
await page.getByRole('combobox', { name: /{{step2Field}}/i }).selectOption('{{step2Value}}');
await page.getByRole('textbox', { name: /{{step2TextField}}/i }).fill('{{step2TextValue}}');
await page.getByRole('button', { name: /next/i }).click();
}
test.describe('Multi-Step Form', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{wizardPath}}');
});
// Happy path: complete all steps
test('completes all wizard steps and submits', async ({ page }) => {
await expect(page.getByText(/step 1/i)).toBeVisible();
await completeStep1(page);
await expect(page.getByText(/step 2/i)).toBeVisible();
await completeStep2(page);
await expect(page.getByText(/review|step 3/i)).toBeVisible();
// Review page shows entered data
await expect(page.getByText('{{firstName}}')).toBeVisible();
await page.getByRole('button', { name: /submit|finish/i }).click();
await expect(page).toHaveURL(/\/{{successPath}}/);
});
// Happy path: step indicator updates
test('step indicator reflects current step', async ({ page }) => {
const step1 = page.getByRole('listitem', { name: /step 1/i });
await expect(step1).toHaveAttribute('aria-current', 'step');
await completeStep1(page);
const step2 = page.getByRole('listitem', { name: /step 2/i });
await expect(step2).toHaveAttribute('aria-current', 'step');
});
// Happy path: back navigation
test('navigates back to previous step without losing data', async ({ page }) => {
await completeStep1(page);
await page.getByRole('button', { name: /back|previous/i }).click();
await expect(page.getByRole('textbox', { name: /first name/i })).toHaveValue('{{firstName}}');
});
// Happy path: completed steps accessible via indicator
test('clicking completed step in indicator navigates back', async ({ page }) => {
await completeStep1(page);
await page.getByRole('button', { name: /step 1/i }).click();
await expect(page.getByRole('textbox', { name: /first name/i })).toBeVisible();
});
// Error case: cannot proceed with invalid step 1 data
test('stays on step 1 when required field missing', async ({ page }) => {
await page.getByRole('button', { name: /next/i }).click();
await expect(page.getByText(/first name.*required|required/i)).toBeVisible();
await expect(page.getByText(/step 1/i)).toBeVisible();
});
// Error case: future step not accessible directly
test('cannot access step 3 without completing step 2', async ({ page }) => {
await expect(page.getByRole('button', { name: /step 3/i })).toBeDisabled();
});
// Edge case: browser back button handled
test('browser back from step 2 returns to step 1 with data', async ({ page }) => {
await completeStep1(page);
await page.goBack();
await expect(page.getByRole('textbox', { name: /first name/i })).toHaveValue('{{firstName}}');
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Multi-Step Form', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{wizardPath}}');
});
test('completes all wizard steps and submits', async ({ page }) => {
await page.getByRole('textbox', { name: /first name/i }).fill('{{firstName}}');
await page.getByRole('textbox', { name: /email/i }).fill('{{email}}');
await page.getByRole('button', { name: /next/i }).click();
await page.getByRole('combobox', { name: /{{step2Field}}/i }).selectOption('{{step2Value}}');
await page.getByRole('button', { name: /next/i }).click();
await page.getByRole('button', { name: /submit|finish/i }).click();
await expect(page).toHaveURL(/\/{{successPath}}/);
});
test('stays on step 1 when required field missing', async ({ page }) => {
await page.getByRole('button', { name: /next/i }).click();
await expect(page.getByText(/required/i)).toBeVisible();
});
test('navigates back without losing data', async ({ page }) => {
await page.getByRole('textbox', { name: /first name/i }).fill('{{firstName}}');
await page.getByRole('textbox', { name: /email/i }).fill('{{email}}');
await page.getByRole('button', { name: /next/i }).click();
await page.getByRole('button', { name: /back|previous/i }).click();
await expect(page.getByRole('textbox', { name: /first name/i })).toHaveValue('{{firstName}}');
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Full completion | All steps filled → submit → success URL |
| Step indicator | aria-current updates per step |
| Back navigation | Data preserved on back |
| Completed step click | Step indicator link works |
| Validation | Required field blocks Next |
| Locked future step | Step 3 button disabled until step 2 done |
| Browser back | History navigation preserves data |
FILE:templates/forms/single-step.md
# Single-Step Form Template
Tests simple form submission with success and validation scenarios.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Form at `{{baseUrl}}/{{formPath}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Single-Step Form — {{formName}}', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
});
// Happy path: successful submission
test('submits form with valid data', async ({ page }) => {
await page.getByRole('textbox', { name: /{{field1Label}}/i }).fill('{{field1Value}}');
await page.getByRole('textbox', { name: /{{field2Label}}/i }).fill('{{field2Value}}');
await page.getByRole('combobox', { name: /{{field3Label}}/i }).selectOption('{{field3Value}}');
await page.getByRole('button', { name: /submit|save/i }).click();
await expect(page.getByRole('alert')).toContainText(/submitted|saved successfully/i);
});
// Happy path: success redirect
test('redirects to success page after submission', async ({ page }) => {
await page.getByRole('textbox', { name: /{{field1Label}}/i }).fill('{{field1Value}}');
await page.getByRole('button', { name: /submit|save/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/{{successPath}}');
});
// Happy path: reset clears form
test('reset button clears all fields', async ({ page }) => {
await page.getByRole('textbox', { name: /{{field1Label}}/i }).fill('some value');
await page.getByRole('button', { name: /reset|clear/i }).click();
await expect(page.getByRole('textbox', { name: /{{field1Label}}/i })).toHaveValue('');
});
// Error case: required field missing
test('shows required field error', async ({ page }) => {
await page.getByRole('button', { name: /submit|save/i }).click();
await expect(page.getByText(/{{field1Label}}.*required|required/i)).toBeVisible();
await expect(page.getByRole('textbox', { name: /{{field1Label}}/i })).toBeFocused();
});
// Error case: invalid email format
test('shows format error for invalid email', async ({ page }) => {
await page.getByRole('textbox', { name: /email/i }).fill('not-an-email');
await page.getByRole('button', { name: /submit|save/i }).click();
await expect(page.getByText(/valid.*email|invalid.*email/i)).toBeVisible();
});
// Error case: server error on submit
test('shows generic error when server returns 500', async ({ page }) => {
await page.route('{{baseUrl}}/api/{{formEndpoint}}', route =>
route.fulfill({ status: 500, body: JSON.stringify({ error: 'Server Error' }) })
);
await page.getByRole('textbox', { name: /{{field1Label}}/i }).fill('{{field1Value}}');
await page.getByRole('button', { name: /submit|save/i }).click();
await expect(page.getByRole('alert')).toContainText(/error|something went wrong/i);
});
// Edge case: double submit prevented
test('disables submit button after first click', async ({ page }) => {
await page.getByRole('textbox', { name: /{{field1Label}}/i }).fill('{{field1Value}}');
const btn = page.getByRole('button', { name: /submit|save/i });
await btn.click();
await expect(btn).toBeDisabled();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Single-Step Form — {{formName}}', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
});
test('submits form with valid data', async ({ page }) => {
await page.getByRole('textbox', { name: /{{field1Label}}/i }).fill('{{field1Value}}');
await page.getByRole('textbox', { name: /{{field2Label}}/i }).fill('{{field2Value}}');
await page.getByRole('button', { name: /submit|save/i }).click();
await expect(page.getByRole('alert')).toContainText(/submitted|saved/i);
});
test('shows required error for empty submission', async ({ page }) => {
await page.getByRole('button', { name: /submit|save/i }).click();
await expect(page.getByText(/required/i)).toBeVisible();
});
test('disables submit after click (prevents double submit)', async ({ page }) => {
await page.getByRole('textbox', { name: /{{field1Label}}/i }).fill('{{field1Value}}');
const btn = page.getByRole('button', { name: /submit|save/i });
await btn.click();
await expect(btn).toBeDisabled();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Valid submit | All fields filled → success message |
| Success redirect | Navigates to success URL |
| Reset | All fields cleared |
| Required field | Empty submit → first error focused |
| Invalid email | Format error shown |
| Server 500 | Generic error alert |
| Double submit | Button disabled after first click |
FILE:templates/forms/validation.md
# Form Validation Template
Tests required fields, format validation, and inline error messages.
## Prerequisites
- Form at `{{baseUrl}}/{{formPath}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Form Validation', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
});
// Happy path: all errors resolved on re-submit
test('clears errors when valid data entered', async ({ page }) => {
await page.getByRole('button', { name: /submit/i }).click();
await expect(page.getByText(/required/i)).toBeVisible();
await page.getByRole('textbox', { name: /{{requiredField}}/i }).fill('{{validValue}}');
await page.getByRole('button', { name: /submit/i }).click();
await expect(page.getByText(/required/i)).toBeHidden();
});
// Error case: required fields
test('shows required error for each empty required field', async ({ page }) => {
await page.getByRole('button', { name: /submit/i }).click();
const requiredErrors = page.getByText(/is required|required field/i);
await expect(requiredErrors.first()).toBeVisible();
});
// Error case: invalid email format
test('shows error for invalid email format', async ({ page }) => {
await page.getByRole('textbox', { name: /email/i }).fill('bad@');
await page.getByRole('textbox', { name: /email/i }).blur();
await expect(page.getByText(/valid.*email|enter.*valid email/i)).toBeVisible();
});
// Error case: invalid phone format
test('shows error for invalid phone number', async ({ page }) => {
await page.getByRole('textbox', { name: /phone/i }).fill('123');
await page.getByRole('textbox', { name: /phone/i }).blur();
await expect(page.getByText(/valid.*phone|invalid phone/i)).toBeVisible();
});
// Error case: password too short
test('shows error when password is too short', async ({ page }) => {
await page.getByRole('textbox', { name: /^password$/i }).fill('abc');
await page.getByRole('textbox', { name: /^password$/i }).blur();
await expect(page.getByText(/at least \d+ characters/i)).toBeVisible();
});
// Error case: passwords do not match
test('shows error when confirm password does not match', async ({ page }) => {
await page.getByRole('textbox', { name: /^password$/i }).fill('{{validPassword}}');
await page.getByRole('textbox', { name: /confirm password/i }).fill('different');
await page.getByRole('textbox', { name: /confirm password/i }).blur();
await expect(page.getByText(/passwords.*do not match/i)).toBeVisible();
});
// Error case: inline error on blur (not on submit)
test('shows inline error on blur for invalid value', async ({ page }) => {
await page.getByRole('textbox', { name: /email/i }).fill('invalid');
await page.getByRole('textbox', { name: /email/i }).blur();
// Error shown immediately, not waiting for submit
await expect(page.getByText(/valid.*email/i)).toBeVisible();
});
// Error case: field-level errors tied to field via aria-describedby
test('error message is associated with field via aria', async ({ page }) => {
await page.getByRole('button', { name: /submit/i }).click();
const emailField = page.getByRole('textbox', { name: /email/i });
const errorId = await emailField.getAttribute('aria-describedby');
expect(errorId).toBeTruthy();
await expect(page.locator(`#errorId`)).toBeVisible();
});
// Edge case: field max-length validation
test('shows error when input exceeds max length', async ({ page }) => {
await page.getByRole('textbox', { name: /{{field}}/i }).fill('A'.repeat({{maxLength}} + 1));
await page.getByRole('textbox', { name: /{{field}}/i }).blur();
await expect(page.getByText(/max.*{{maxLength}}|too long/i)).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Form Validation', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
});
test('shows required errors on empty submit', async ({ page }) => {
await page.getByRole('button', { name: /submit/i }).click();
await expect(page.getByText(/is required|required field/i).first()).toBeVisible();
});
test('shows error for invalid email on blur', async ({ page }) => {
await page.getByRole('textbox', { name: /email/i }).fill('bad@');
await page.getByRole('textbox', { name: /email/i }).blur();
await expect(page.getByText(/valid.*email/i)).toBeVisible();
});
test('passwords mismatch error shown', async ({ page }) => {
await page.getByRole('textbox', { name: /^password$/i }).fill('{{validPassword}}');
await page.getByRole('textbox', { name: /confirm password/i }).fill('other');
await page.getByRole('textbox', { name: /confirm password/i }).blur();
await expect(page.getByText(/do not match/i)).toBeVisible();
});
test('clears errors when valid data entered', async ({ page }) => {
await page.getByRole('button', { name: /submit/i }).click();
await page.getByRole('textbox', { name: /{{requiredField}}/i }).fill('{{validValue}}');
await page.getByRole('button', { name: /submit/i }).click();
await expect(page.getByText(/required/i)).toBeHidden();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Error cleared | Valid input → errors removed on next submit |
| Required fields | Empty submit → at least one required error |
| Email format | Blur with bad email → inline error |
| Phone format | Invalid phone → inline error |
| Password length | Too short → character count error |
| Password match | Mismatch → confirmation error |
| Blur validation | Error shown on blur, not just submit |
| aria-describedby | Error programmatically linked to field |
| Max length | Exceeded length → error shown |
FILE:templates/notifications/in-app.md
# In-App Notifications Template
Tests notification badge count, dropdown, and mark-as-read behaviour.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- At least `{{unreadCount}}` unread notifications seeded
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('In-App Notifications', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
// Happy path: badge shows unread count
test('shows unread notification count badge', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByRole('status', { name: /notification.*count/i }))
.toContainText('{{unreadCount}}');
});
// Happy path: dropdown opens on bell click
test('opens notification dropdown on bell click', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /notifications/i }).click();
await expect(page.getByRole('menu', { name: /notifications/i })).toBeVisible();
const items = page.getByRole('menuitem');
await expect(items.first()).toBeVisible();
});
// Happy path: mark single notification as read
test('marks notification as read', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /notifications/i }).click();
const firstNotif = page.getByRole('menuitem').first();
await firstNotif.getByRole('button', { name: /mark as read/i }).click();
await expect(firstNotif).toHaveAttribute('aria-label', /read/i);
// Badge count decremented
await expect(page.getByRole('status', { name: /notification.*count/i }))
.toContainText(`{{unreadCount} - 1}`);
});
// Happy path: mark all as read
test('marks all notifications as read', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /notifications/i }).click();
await page.getByRole('button', { name: /mark all.*read/i }).click();
await expect(page.getByRole('status', { name: /notification.*count/i })).toBeHidden();
});
// Happy path: clicking notification navigates to context
test('clicking notification navigates to relevant page', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /notifications/i }).click();
await page.getByRole('menuitem').first().click();
await expect(page).toHaveURL(/\/{{notificationTargetPath}}/);
});
// Error case: notification dropdown empty state
test('shows empty state when no notifications', async ({ page }) => {
await page.route('{{baseUrl}}/api/notifications*', route =>
route.fulfill({ status: 200, body: JSON.stringify({ items: [], unread: 0 }) })
);
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /notifications/i }).click();
await expect(page.getByText(/no notifications|all caught up/i)).toBeVisible();
});
// Edge case: dropdown closes on outside click
test('closes notification dropdown on outside click', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /notifications/i }).click();
await expect(page.getByRole('menu', { name: /notifications/i })).toBeVisible();
await page.getByRole('heading', { name: /dashboard/i }).click();
await expect(page.getByRole('menu', { name: /notifications/i })).toBeHidden();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('In-App Notifications', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('badge shows unread count', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByRole('status', { name: /notification.*count/i }))
.toContainText('{{unreadCount}}');
});
test('opens dropdown on bell click', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /notifications/i }).click();
await expect(page.getByRole('menu', { name: /notifications/i })).toBeVisible();
});
test('marks all as read clears badge', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /notifications/i }).click();
await page.getByRole('button', { name: /mark all.*read/i }).click();
await expect(page.getByRole('status', { name: /notification.*count/i })).toBeHidden();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Badge count | Unread count shown in badge |
| Dropdown open | Bell click → notification list |
| Mark single read | Item marked, badge decremented |
| Mark all read | Badge hidden |
| Notification click | Navigates to context page |
| Empty state | No-notifications message |
| Outside click | Dropdown closes |
FILE:templates/notifications/notification-center.md
# Notification Center Template
Tests full notification list, filtering, and bulk clear.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Mix of read/unread notifications seeded
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Notification Center', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/notifications');
});
// Happy path: notification list visible
test('displays notification list', async ({ page }) => {
await expect(page.getByRole('heading', { name: /notifications/i })).toBeVisible();
await expect(page.getByRole('list', { name: /notifications/i })).toBeVisible();
const items = page.getByRole('listitem');
await expect(items.first()).toBeVisible();
});
// Happy path: filter by unread
test('filters to show only unread notifications', async ({ page }) => {
await page.getByRole('button', { name: /unread/i }).click();
const items = page.getByRole('listitem');
const count = await items.count();
for (let i = 0; i < count; i++) {
await expect(items.nth(i)).toHaveAttribute('aria-label', /unread/i);
}
});
// Happy path: filter by type
test('filters notifications by type', async ({ page }) => {
await page.getByRole('combobox', { name: /type|category/i }).selectOption('{{notificationType}}');
const items = page.getByRole('listitem');
await expect(items.first()).toContainText(/{{notificationTypeLabel}}/i);
});
// Happy path: mark single as read
test('marks individual notification as read', async ({ page }) => {
const first = page.getByRole('listitem').first();
await first.getByRole('button', { name: /mark.*read/i }).click();
await expect(first).not.toHaveAttribute('data-unread', 'true');
});
// Happy path: clear all notifications
test('clears all notifications', async ({ page }) => {
await page.getByRole('button', { name: /clear all/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /confirm/i }).click();
await expect(page.getByText(/no notifications|all cleared/i)).toBeVisible();
await expect(page.getByRole('listitem')).toHaveCount(0);
});
// Happy path: pagination / load more
test('loads more notifications on scroll or button click', async ({ page }) => {
const initialCount = await page.getByRole('listitem').count();
await page.getByRole('button', { name: /load more/i }).click();
const newCount = await page.getByRole('listitem').count();
expect(newCount).toBeGreaterThan(initialCount);
});
// Error case: empty state after clearing
test('shows empty state after clearing all', async ({ page }) => {
await page.getByRole('button', { name: /clear all/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /confirm/i }).click();
await expect(page.getByText(/no notifications/i)).toBeVisible();
});
// Edge case: notification links to source
test('clicking notification navigates to source', async ({ page }) => {
await page.getByRole('listitem').first().getByRole('link').click();
await expect(page).not.toHaveURL('{{baseUrl}}/notifications');
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Notification Center', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('displays notification list', async ({ page }) => {
await page.goto('{{baseUrl}}/notifications');
await expect(page.getByRole('list', { name: /notifications/i })).toBeVisible();
await expect(page.getByRole('listitem').first()).toBeVisible();
});
test('filters to unread only', async ({ page }) => {
await page.goto('{{baseUrl}}/notifications');
await page.getByRole('button', { name: /unread/i }).click();
await expect(page.getByRole('listitem').first()).toHaveAttribute('aria-label', /unread/i);
});
test('clears all notifications', async ({ page }) => {
await page.goto('{{baseUrl}}/notifications');
await page.getByRole('button', { name: /clear all/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /confirm/i }).click();
await expect(page.getByText(/no notifications/i)).toBeVisible();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| List display | Notification items visible |
| Unread filter | Only unread items shown |
| Type filter | Category filter scopes list |
| Mark single read | Item marked, styling changes |
| Clear all | Confirmation → empty state |
| Load more | Additional items appended |
| Empty state | No-notifications message post-clear |
| Source link | Click navigates away from center |
FILE:templates/notifications/toast-messages.md
# Toast Messages Template
Tests success, error, and warning toasts with auto-dismiss and manual close.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Toast Messages', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
// Happy path: success toast on action
test('shows success toast after save action', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
await page.getByRole('textbox', { name: /{{fieldLabel}}/i }).fill('{{validValue}}');
await page.getByRole('button', { name: /save/i }).click();
const toast = page.getByRole('alert').filter({ hasText: /saved|success/i });
await expect(toast).toBeVisible();
});
// Happy path: error toast on failure
test('shows error toast when action fails', async ({ page }) => {
await page.route('{{baseUrl}}/api/{{endpoint}}*', route =>
route.fulfill({ status: 500, body: '{}' })
);
await page.goto('{{baseUrl}}/{{formPath}}');
await page.getByRole('button', { name: /save/i }).click();
const toast = page.getByRole('alert').filter({ hasText: /error|failed/i });
await expect(toast).toBeVisible();
});
// Happy path: warning toast shown
test('shows warning toast', async ({ page }) => {
await page.goto('{{baseUrl}}/{{warningTriggerPath}}');
await page.getByRole('button', { name: /{{warningAction}}/i }).click();
const toast = page.getByRole('alert').filter({ hasText: /warning|attention/i });
await expect(toast).toBeVisible();
});
// Happy path: toast auto-dismisses
test('toast auto-dismisses after timeout', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
await page.clock.install();
await page.getByRole('textbox', { name: /{{fieldLabel}}/i }).fill('{{validValue}}');
await page.getByRole('button', { name: /save/i }).click();
const toast = page.getByRole('alert').filter({ hasText: /saved/i });
await expect(toast).toBeVisible();
await page.clock.fastForward({{toastDurationMs}});
await expect(toast).toBeHidden();
});
// Happy path: toast manually dismissed
test('dismisses toast via close button', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
await page.getByRole('button', { name: /save/i }).click();
const toast = page.getByRole('alert').filter({ hasText: /saved/i });
await expect(toast).toBeVisible();
await toast.getByRole('button', { name: /close|dismiss|×/i }).click();
await expect(toast).toBeHidden();
});
// Happy path: multiple toasts stack
test('stacks multiple toasts', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
// Trigger two saves quickly
await page.getByRole('button', { name: /save/i }).click();
await page.getByRole('button', { name: /save/i }).click();
const toasts = page.getByRole('alert');
const count = await toasts.count();
expect(count).toBeGreaterThanOrEqual(2);
});
// Edge case: toast announces to screen readers
test('toast has live region role for accessibility', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
await page.getByRole('button', { name: /save/i }).click();
const toast = page.getByRole('alert').first();
await expect(toast).toBeVisible();
// role="alert" implies aria-live="assertive"
const role = await toast.getAttribute('role');
expect(role).toMatch(/alert|status/);
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Toast Messages', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('shows success toast after save', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
await page.getByRole('textbox', { name: /{{fieldLabel}}/i }).fill('{{validValue}}');
await page.getByRole('button', { name: /save/i }).click();
await expect(page.getByRole('alert').filter({ hasText: /saved|success/i })).toBeVisible();
});
test('toast auto-dismisses after timeout', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
await page.clock.install();
await page.getByRole('button', { name: /save/i }).click();
const toast = page.getByRole('alert').filter({ hasText: /saved/i });
await expect(toast).toBeVisible();
await page.clock.fastForward({{toastDurationMs}});
await expect(toast).toBeHidden();
});
test('dismisses toast via close button', async ({ page }) => {
await page.goto('{{baseUrl}}/{{formPath}}');
await page.getByRole('button', { name: /save/i }).click();
const toast = page.getByRole('alert').filter({ hasText: /saved/i });
await toast.getByRole('button', { name: /close|×/i }).click();
await expect(toast).toBeHidden();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Success toast | Save → green/success alert visible |
| Error toast | 500 → red/error alert visible |
| Warning toast | Trigger action → warning alert |
| Auto-dismiss | Toast hidden after N ms (clock-controlled) |
| Manual dismiss | Close button hides toast |
| Stacked toasts | Multiple alerts visible simultaneously |
| Accessible | role=alert or role=status present |
FILE:templates/onboarding/email-verification.md
# Email Verification Template
Tests email verification link, resend flow, and expired token handling.
## Prerequisites
- Registered but unverified account: `{{unverifiedEmail}}`
- Valid token: `{{verificationToken}}`
- Expired token: `{{expiredVerificationToken}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Email Verification', () => {
// Happy path: valid verification link
test('verifies email with valid token', async ({ page }) => {
await page.goto('{{baseUrl}}/verify-email?token={{verificationToken}}');
await expect(page.getByRole('heading', { name: /email verified|verified/i })).toBeVisible();
await expect(page.getByRole('link', { name: /continue|go to dashboard/i })).toBeVisible();
});
// Happy path: continues to app after verification
test('redirects to dashboard after clicking continue', async ({ page }) => {
await page.goto('{{baseUrl}}/verify-email?token={{verificationToken}}');
await page.getByRole('link', { name: /continue|go to dashboard/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
});
// Happy path: resend verification email
test('resends verification email', async ({ page }) => {
await page.goto('{{baseUrl}}/verify-email/resend');
await page.getByRole('textbox', { name: /email/i }).fill('{{unverifiedEmail}}');
await page.getByRole('button', { name: /resend/i }).click();
await expect(page.getByRole('alert')).toContainText(/sent|check your email/i);
});
// Happy path: verification prompt on login for unverified user
test('shows verification prompt when unverified user logs in', async ({ page }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('textbox', { name: /email/i }).fill('{{unverifiedEmail}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{password}}');
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page.getByText(/verify.*email|check.*inbox/i)).toBeVisible();
await expect(page.getByRole('button', { name: /resend.*verification/i })).toBeVisible();
});
// Error case: expired token
test('shows error for expired verification token', async ({ page }) => {
await page.goto('{{baseUrl}}/verify-email?token={{expiredVerificationToken}}');
await expect(page.getByRole('heading', { name: /link.*expired|verification.*failed/i })).toBeVisible();
await expect(page.getByRole('link', { name: /resend|request new/i })).toBeVisible();
});
// Error case: invalid token
test('shows error for invalid verification token', async ({ page }) => {
await page.goto('{{baseUrl}}/verify-email?token=invalid-token-xyz');
await expect(page.getByRole('heading', { name: /invalid|failed/i })).toBeVisible();
});
// Edge case: already verified user hitting link
test('shows already verified message for used token', async ({ page }) => {
await page.goto('{{baseUrl}}/verify-email?token={{usedVerificationToken}}');
await expect(page.getByText(/already verified|email.*confirmed/i)).toBeVisible();
await expect(page.getByRole('link', { name: /sign in/i })).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Email Verification', () => {
test('verifies email with valid token', async ({ page }) => {
await page.goto('{{baseUrl}}/verify-email?token={{verificationToken}}');
await expect(page.getByRole('heading', { name: /email verified/i })).toBeVisible();
});
test('shows error for expired token', async ({ page }) => {
await page.goto('{{baseUrl}}/verify-email?token={{expiredVerificationToken}}');
await expect(page.getByRole('heading', { name: /link.*expired/i })).toBeVisible();
await expect(page.getByRole('link', { name: /resend|request new/i })).toBeVisible();
});
test('resends verification email', async ({ page }) => {
await page.goto('{{baseUrl}}/verify-email/resend');
await page.getByRole('textbox', { name: /email/i }).fill('{{unverifiedEmail}}');
await page.getByRole('button', { name: /resend/i }).click();
await expect(page.getByRole('alert')).toContainText(/sent/i);
});
test('shows verification prompt on login for unverified user', async ({ page }) => {
await page.goto('{{baseUrl}}/login');
await page.getByRole('textbox', { name: /email/i }).fill('{{unverifiedEmail}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{password}}');
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page.getByText(/verify.*email/i)).toBeVisible();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Valid token | Email verified heading + continue link |
| Continue CTA | Navigates to dashboard |
| Resend | Sends new email, success alert |
| Login prompt | Unverified login shows resend button |
| Expired token | Error heading + resend link |
| Invalid token | Generic error heading |
| Already verified | "Already verified" with login link |
FILE:templates/onboarding/first-time-setup.md
# First-Time Setup Template
Tests initial configuration wizard and profile completion after registration.
## Prerequisites
- Newly registered session via `{{newUserStorageStatePath}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('First-Time Setup', () => {
test.use({ storageState: '{{newUserStorageStatePath}}' });
// Happy path: setup wizard shown on first login
test('shows setup wizard on first login', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page).toHaveURL(/\/setup|\/onboarding/);
await expect(page.getByRole('heading', { name: /set up.*account|get started/i })).toBeVisible();
});
// Happy path: complete organisation setup step
test('completes organisation details step', async ({ page }) => {
await page.goto('{{baseUrl}}/setup');
await page.getByRole('textbox', { name: /organisation.*name|company/i }).fill('{{orgName}}');
await page.getByRole('combobox', { name: /industry/i }).selectOption('{{industry}}');
await page.getByRole('spinbutton', { name: /team size/i }).fill('{{teamSize}}');
await page.getByRole('button', { name: /next|continue/i }).click();
await expect(page.getByText(/step 2|preferences/i)).toBeVisible();
});
// Happy path: complete preferences step
test('completes preferences step', async ({ page }) => {
await page.goto('{{baseUrl}}/setup/preferences');
await page.getByRole('combobox', { name: /timezone/i }).selectOption('{{timezone}}');
await page.getByRole('combobox', { name: /language/i }).selectOption('{{language}}');
await page.getByRole('button', { name: /next|continue/i }).click();
await expect(page.getByText(/step 3|invite|done/i)).toBeVisible();
});
// Happy path: full wizard completion redirects to dashboard
test('completes all setup steps and lands on dashboard', async ({ page }) => {
await page.goto('{{baseUrl}}/setup');
// Step 1
await page.getByRole('textbox', { name: /organisation.*name/i }).fill('{{orgName}}');
await page.getByRole('button', { name: /next/i }).click();
// Step 2
await page.getByRole('combobox', { name: /timezone/i }).selectOption('{{timezone}}');
await page.getByRole('button', { name: /next/i }).click();
// Final step
await page.getByRole('button', { name: /finish|go to dashboard/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
});
// Happy path: setup completion percentage shown
test('progress indicator updates on each step', async ({ page }) => {
await page.goto('{{baseUrl}}/setup');
await expect(page.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '0');
await page.getByRole('textbox', { name: /organisation.*name/i }).fill('{{orgName}}');
await page.getByRole('button', { name: /next/i }).click();
await expect(page.getByRole('progressbar')).not.toHaveAttribute('aria-valuenow', '0');
});
// Error case: required setup field missing
test('shows validation when required field missing', async ({ page }) => {
await page.goto('{{baseUrl}}/setup');
await page.getByRole('button', { name: /next/i }).click();
await expect(page.getByText(/organisation.*required|required/i)).toBeVisible();
});
// Edge case: setup not required on subsequent login
test('skips setup on second login', async ({ page }) => {
// Complete setup
await page.goto('{{baseUrl}}/setup');
await page.getByRole('textbox', { name: /organisation.*name/i }).fill('{{orgName}}');
await page.getByRole('button', { name: /next/i }).click();
await page.getByRole('button', { name: /finish/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
// Reload — setup not re-triggered
await page.reload();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('First-Time Setup', () => {
test.use({ storageState: '{{newUserStorageStatePath}}' });
test('redirects to setup wizard on first login', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page).toHaveURL(/\/setup|\/onboarding/);
});
test('shows validation for missing required field', async ({ page }) => {
await page.goto('{{baseUrl}}/setup');
await page.getByRole('button', { name: /next/i }).click();
await expect(page.getByText(/required/i)).toBeVisible();
});
test('completes setup and lands on dashboard', async ({ page }) => {
await page.goto('{{baseUrl}}/setup');
await page.getByRole('textbox', { name: /organisation.*name/i }).fill('{{orgName}}');
await page.getByRole('button', { name: /next/i }).click();
await page.getByRole('button', { name: /finish|go to dashboard/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Setup on first login | Redirected to /setup wizard |
| Org details step | Company name + industry filled |
| Preferences step | Timezone + language selected |
| Full completion | All steps → dashboard |
| Progress bar | Progressbar value updates per step |
| Required field | Empty step blocked with error |
| Skip on re-login | Setup not triggered again |
FILE:templates/onboarding/registration.md
# Registration Template
Tests signup form submission, validation, and post-registration flow.
## Prerequisites
- Unique test email for each run: `{{newUserEmail}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
const uniqueEmail = `test+Date.now()@example.com`;
test.describe('Registration', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/register');
});
// Happy path: successful registration
test('registers new user with valid data', async ({ page }) => {
await page.getByRole('textbox', { name: /first name/i }).fill('{{firstName}}');
await page.getByRole('textbox', { name: /last name/i }).fill('{{lastName}}');
await page.getByRole('textbox', { name: /email/i }).fill(uniqueEmail);
await page.getByRole('textbox', { name: /^password$/i }).fill('{{newPassword}}');
await page.getByRole('textbox', { name: /confirm.*password/i }).fill('{{newPassword}}');
await page.getByRole('checkbox', { name: /terms/i }).check();
await page.getByRole('button', { name: /sign up|register|create account/i }).click();
await expect(page).toHaveURL(/\/verify-email|\/dashboard|\/onboarding/);
});
// Happy path: success message or redirect
test('shows confirmation after registration', async ({ page }) => {
await page.getByRole('textbox', { name: /email/i }).fill(uniqueEmail);
await page.getByRole('textbox', { name: /^password$/i }).fill('{{newPassword}}');
await page.getByRole('checkbox', { name: /terms/i }).check();
await page.getByRole('button', { name: /sign up|register/i }).click();
await expect(page.getByText(/check your email|account created|welcome/i)).toBeVisible();
});
// Error case: email already registered
test('shows error for already registered email', async ({ page }) => {
await page.getByRole('textbox', { name: /email/i }).fill('{{existingUserEmail}}');
await page.getByRole('textbox', { name: /^password$/i }).fill('{{newPassword}}');
await page.getByRole('checkbox', { name: /terms/i }).check();
await page.getByRole('button', { name: /sign up|register/i }).click();
await expect(page.getByRole('alert')).toContainText(/already.*registered|email.*taken/i);
});
// Error case: terms not accepted
test('blocks registration if terms not accepted', async ({ page }) => {
await page.getByRole('textbox', { name: /email/i }).fill(uniqueEmail);
await page.getByRole('textbox', { name: /^password$/i }).fill('{{newPassword}}');
await page.getByRole('button', { name: /sign up|register/i }).click();
await expect(page.getByText(/accept.*terms|terms.*required/i)).toBeVisible();
});
// Error case: weak password
test('shows error for weak password', async ({ page }) => {
await page.getByRole('textbox', { name: /^password$/i }).fill('123');
await page.getByRole('textbox', { name: /^password$/i }).blur();
await expect(page.getByText(/at least \d+ characters|too weak/i)).toBeVisible();
});
// Error case: passwords mismatch
test('shows error when passwords do not match', async ({ page }) => {
await page.getByRole('textbox', { name: /^password$/i }).fill('{{newPassword}}');
await page.getByRole('textbox', { name: /confirm.*password/i }).fill('different');
await page.getByRole('textbox', { name: /confirm.*password/i }).blur();
await expect(page.getByText(/do not match/i)).toBeVisible();
});
// Edge case: already logged-in user redirected
test('redirects to dashboard when already authenticated', async ({ page, context }) => {
await context.addCookies([{ name: '{{sessionCookieName}}', value: '{{validSession}}', domain: '{{cookieDomain}}', path: '/' }]);
await page.goto('{{baseUrl}}/register');
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Registration', () => {
test('registers with valid data', async ({ page }) => {
const email = `test+Date.now()@example.com`;
await page.goto('{{baseUrl}}/register');
await page.getByRole('textbox', { name: /email/i }).fill(email);
await page.getByRole('textbox', { name: /^password$/i }).fill('{{newPassword}}');
await page.getByRole('checkbox', { name: /terms/i }).check();
await page.getByRole('button', { name: /sign up|register/i }).click();
await expect(page).toHaveURL(/\/verify-email|\/dashboard|\/onboarding/);
});
test('shows error for existing email', async ({ page }) => {
await page.goto('{{baseUrl}}/register');
await page.getByRole('textbox', { name: /email/i }).fill('{{existingUserEmail}}');
await page.getByRole('textbox', { name: /^password$/i }).fill('{{newPassword}}');
await page.getByRole('checkbox', { name: /terms/i }).check();
await page.getByRole('button', { name: /sign up|register/i }).click();
await expect(page.getByRole('alert')).toContainText(/already.*registered/i);
});
test('requires terms acceptance', async ({ page }) => {
await page.goto('{{baseUrl}}/register');
await page.getByRole('textbox', { name: /email/i }).fill(`tDate.now()@example.com`);
await page.getByRole('textbox', { name: /^password$/i }).fill('{{newPassword}}');
await page.getByRole('button', { name: /sign up|register/i }).click();
await expect(page.getByText(/accept.*terms/i)).toBeVisible();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Valid registration | All fields → redirect or success message |
| Confirmation | Email check or welcome shown |
| Existing email | Error alert |
| Terms not accepted | Validation error |
| Weak password | Strength error on blur |
| Password mismatch | Confirm error |
| Already authed | Redirected to dashboard |
FILE:templates/onboarding/welcome-tour.md
# Welcome Tour Template
Tests step-by-step onboarding tour, skip, and completion behaviour.
## Prerequisites
- Newly registered session (first login) via `{{newUserStorageStatePath}}`
- Tour has `{{tourStepCount}}` steps
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Welcome Tour', () => {
test.use({ storageState: '{{newUserStorageStatePath}}' });
// Happy path: tour shown on first login
test('shows welcome tour on first login', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByRole('dialog', { name: /welcome|tour/i })).toBeVisible();
await expect(page.getByText(/step 1 of {{tourStepCount}}/i)).toBeVisible();
});
// Happy path: advance through all steps
test('advances through all tour steps', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
for (let i = 1; i <= {{tourStepCount}}; i++) {
await expect(page.getByText(new RegExp(`step i of {{tourStepCount}}`, 'i'))).toBeVisible();
if (i < {{tourStepCount}}) {
await page.getByRole('button', { name: /next/i }).click();
} else {
await page.getByRole('button', { name: /finish|done|get started/i }).click();
}
}
await expect(page.getByRole('dialog', { name: /welcome|tour/i })).toBeHidden();
});
// Happy path: back navigation within tour
test('navigates back to previous step', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /next/i }).click();
await expect(page.getByText(/step 2 of {{tourStepCount}}/i)).toBeVisible();
await page.getByRole('button', { name: /back|previous/i }).click();
await expect(page.getByText(/step 1 of {{tourStepCount}}/i)).toBeVisible();
});
// Happy path: skip tour
test('skips tour and dismisses overlay', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /skip.*tour|skip/i }).click();
await expect(page.getByRole('dialog', { name: /welcome|tour/i })).toBeHidden();
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
});
// Happy path: tour not shown on subsequent logins
test('tour not shown on second login', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
// Complete or skip tour
await page.getByRole('button', { name: /skip.*tour|skip/i }).click();
// Simulate re-login by reloading
await page.reload();
await expect(page.getByRole('dialog', { name: /welcome|tour/i })).toBeHidden();
});
// Happy path: tooltip highlights correct element
test('tour tooltip highlights the correct UI element', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
const tooltip = page.getByRole('tooltip').or(page.getByRole('dialog', { name: /tour/i }));
await expect(tooltip).toBeVisible();
const targetEl = page.getByRole('{{tourStep1TargetRole}}', { name: /{{tourStep1TargetName}}/i });
await expect(targetEl).toBeVisible();
});
// Edge case: close button (×) dismisses tour
test('× button dismisses tour', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('dialog', { name: /welcome|tour/i })
.getByRole('button', { name: /close|×/i }).click();
await expect(page.getByRole('dialog', { name: /welcome|tour/i })).toBeHidden();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Welcome Tour', () => {
test.use({ storageState: '{{newUserStorageStatePath}}' });
test('shows welcome tour on first login', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await expect(page.getByRole('dialog', { name: /welcome|tour/i })).toBeVisible();
});
test('skips tour on button click', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
await page.getByRole('button', { name: /skip/i }).click();
await expect(page.getByRole('dialog', { name: /tour/i })).toBeHidden();
});
test('advances through all steps to completion', async ({ page }) => {
await page.goto('{{baseUrl}}/dashboard');
for (let i = 1; i < {{tourStepCount}}; i++) {
await page.getByRole('button', { name: /next/i }).click();
}
await page.getByRole('button', { name: /finish|done|get started/i }).click();
await expect(page.getByRole('dialog', { name: /tour/i })).toBeHidden();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Tour on first login | Dialog shown with step 1 of N |
| Full completion | All steps advanced → tour dismissed |
| Back navigation | Previous step accessible |
| Skip tour | Dismissed immediately |
| Not shown again | Tour absent on subsequent visits |
| Tooltip target | Tour highlights correct element |
| Close button | × closes tour |
FILE:templates/search/basic-search.md
# Basic Search Template
Tests search input, query submission, and results display.
## Prerequisites
- At least one indexed item matching `{{searchQuery}}`
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Basic Search', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}');
});
// Happy path: search returns results
test('displays results for valid search query', async ({ page }) => {
await page.getByRole('searchbox', { name: /search/i }).fill('{{searchQuery}}');
await page.getByRole('button', { name: /search/i }).click();
await expect(page).toHaveURL(/[?&]q={{searchQuery}}/);
await expect(page.getByRole('list', { name: /results/i })).toBeVisible();
const results = page.getByRole('listitem').filter({ hasText: /{{searchQuery}}/i });
await expect(results.first()).toBeVisible();
});
// Happy path: search via Enter key
test('submits search on Enter key', async ({ page }) => {
await page.getByRole('searchbox', { name: /search/i }).fill('{{searchQuery}}');
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/[?&]q=/);
await expect(page.getByRole('list', { name: /results/i })).toBeVisible();
});
// Happy path: result count shown
test('shows result count in heading', async ({ page }) => {
await page.getByRole('searchbox', { name: /search/i }).fill('{{searchQuery}}');
await page.getByRole('button', { name: /search/i }).click();
await expect(page.getByText(/\d+\s+results? for/i)).toBeVisible();
});
// Happy path: clicking result navigates to detail
test('clicking result navigates to detail page', async ({ page }) => {
await page.getByRole('searchbox', { name: /search/i }).fill('{{searchQuery}}');
await page.getByRole('button', { name: /search/i }).click();
await page.getByRole('listitem').first().getByRole('link').click();
await expect(page).toHaveURL(/\/{{entityName}}s\/\d+/);
});
// Happy path: query pre-filled from URL
test('pre-fills search box from URL query param', async ({ page }) => {
await page.goto(`{{baseUrl}}/search?q={{searchQuery}}`);
await expect(page.getByRole('searchbox', { name: /search/i })).toHaveValue('{{searchQuery}}');
});
// Error case: no results
test('shows no-results message for unmatched query', async ({ page }) => {
await page.getByRole('searchbox', { name: /search/i }).fill('xyzzy-no-match-12345');
await page.getByRole('button', { name: /search/i }).click();
await expect(page.getByText(/no results|nothing found/i)).toBeVisible();
});
// Edge case: special characters handled safely
test('handles special characters in query', async ({ page }) => {
await page.getByRole('searchbox', { name: /search/i }).fill('<script>alert(1)</script>');
await page.getByRole('button', { name: /search/i }).click();
await expect(page.getByRole('alert')).toBeHidden();
await expect(page.getByText(/no results/i)).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Basic Search', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}');
});
test('displays results for valid query', async ({ page }) => {
await page.getByRole('searchbox', { name: /search/i }).fill('{{searchQuery}}');
await page.getByRole('button', { name: /search/i }).click();
await expect(page.getByRole('list', { name: /results/i })).toBeVisible();
});
test('shows no-results for unmatched query', async ({ page }) => {
await page.getByRole('searchbox', { name: /search/i }).fill('xyzzy-no-match');
await page.getByRole('button', { name: /search/i }).click();
await expect(page.getByText(/no results|nothing found/i)).toBeVisible();
});
test('submits on Enter key', async ({ page }) => {
await page.getByRole('searchbox', { name: /search/i }).fill('{{searchQuery}}');
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/[?&]q=/);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Valid query | Results list visible, count shown |
| Enter key | Search submitted without clicking button |
| Result count | Heading shows N results for query |
| Result click | Navigates to entity detail |
| URL pre-fill | Query param populates search box |
| No results | Empty state message |
| Special chars | XSS input handled, no script execution |
FILE:templates/search/empty-state.md
# Empty State Template
Tests no-results messaging and clear-filters behaviour.
## Prerequisites
- App running at `{{baseUrl}}`
- Query that returns no results: `{{emptySearchQuery}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Empty State', () => {
// Happy path: no results message
test('shows no-results message for unmatched query', async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{emptySearchQuery}}');
await expect(page.getByRole('heading', { name: /no results|nothing found/i })).toBeVisible();
await expect(page.getByText(/try.*different|adjust.*search/i)).toBeVisible();
});
// Happy path: clear filters CTA shown in empty state
test('shows "clear filters" button when filters applied with no results', async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{searchQuery}}&category={{nonExistentCategory}}');
await expect(page.getByText(/no results/i)).toBeVisible();
await expect(page.getByRole('button', { name: /clear.*filter/i })).toBeVisible();
});
// Happy path: clearing filters restores results
test('clearing filters from empty state restores results', async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{searchQuery}}&category={{nonExistentCategory}}');
await page.getByRole('button', { name: /clear.*filter/i }).click();
await expect(page.getByRole('listitem').first()).toBeVisible();
await expect(page.getByText(/no results/i)).toBeHidden();
});
// Happy path: search suggestions shown in empty state
test('shows related search suggestions', async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{emptySearchQuery}}');
const suggestions = page.getByRole('list', { name: /suggestions|similar/i });
if (await suggestions.isVisible()) {
await expect(suggestions.getByRole('listitem').first()).toBeVisible();
}
});
// Happy path: empty list view (not search)
test('shows empty state on entity list with no data', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s?filter={{emptyFilter}}');
await expect(page.getByText(/no {{entityName}}s|empty/i)).toBeVisible();
await expect(page.getByRole('button', { name: /create|add new/i })).toBeVisible();
});
// Error case: network error shows error state not empty state
test('distinguishes network error from no-results', async ({ page }) => {
await page.route('{{baseUrl}}/api/search*', route => route.abort('failed'));
await page.goto('{{baseUrl}}/search?q={{searchQuery}}');
await expect(page.getByText(/error|something went wrong/i)).toBeVisible();
await expect(page.getByText(/no results/i)).toBeHidden();
});
// Edge case: empty state after removing last item
test('shows empty state after deleting last item in list', async ({ page }) => {
await page.goto('{{baseUrl}}/{{entityName}}s');
const row = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') }).last();
await row.getByRole('button', { name: /delete/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /confirm/i }).click();
await expect(page.getByText(/no {{entityName}}s|empty/i)).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Empty State', () => {
test('shows no-results message for unmatched query', async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{emptySearchQuery}}');
await expect(page.getByRole('heading', { name: /no results|nothing found/i })).toBeVisible();
});
test('shows clear-filters button in no-results state', async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{searchQuery}}&category={{nonExistentCategory}}');
await expect(page.getByRole('button', { name: /clear.*filter/i })).toBeVisible();
});
test('clearing filters restores results', async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{searchQuery}}&category={{nonExistentCategory}}');
await page.getByRole('button', { name: /clear.*filter/i }).click();
await expect(page.getByRole('listitem').first()).toBeVisible();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| No-results query | Heading + suggestion text shown |
| Filter no-results | Clear-filters CTA displayed |
| Clear filters | Removes filter, results return |
| Search suggestions | Related terms listed when available |
| Empty list view | Entity list empty state with create CTA |
| Network error | Error state distinct from no-results |
| Last item deleted | Empty state shown after deletion |
FILE:templates/search/filters.md
# Search Filters Template
Tests category filter, price range, and checkbox filters.
## Prerequisites
- Search results available for `{{searchQuery}}`
- Category `{{filterCategory}}` with items
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Search Filters', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{searchQuery}}');
});
// Happy path: category filter
test('filters results by category', async ({ page }) => {
await page.getByRole('checkbox', { name: '{{filterCategory}}' }).check();
await expect(page).toHaveURL(/category={{filterCategory}}/);
const results = page.getByRole('listitem');
await expect(results.first()).toContainText('{{filterCategory}}');
const count = await results.count();
expect(count).toBeGreaterThan(0);
});
// Happy path: price range filter
test('filters results by price range', async ({ page }) => {
const minInput = page.getByRole('spinbutton', { name: /min.*price/i });
const maxInput = page.getByRole('spinbutton', { name: /max.*price/i });
await minInput.fill('{{minPrice}}');
await maxInput.fill('{{maxPrice}}');
await page.getByRole('button', { name: /apply|filter/i }).click();
await expect(page).toHaveURL(/min_price={{minPrice}}/);
// Verify no results exceed max price
const prices = page.getByTestId('item-price');
const priceCount = await prices.count();
for (let i = 0; i < priceCount; i++) {
const text = await prices.nth(i).textContent() ?? '';
const value = parseFloat(text.replace(/[^0-9.]/g, ''));
expect(value).toBeLessThanOrEqual({{maxPrice}});
}
});
// Happy path: multiple checkboxes combine filters
test('applies multiple checkbox filters simultaneously', async ({ page }) => {
await page.getByRole('checkbox', { name: '{{filterOption1}}' }).check();
await page.getByRole('checkbox', { name: '{{filterOption2}}' }).check();
await expect(page).toHaveURL(/{{filterParam1}}.*{{filterParam2}}|{{filterParam2}}.*{{filterParam1}}/);
});
// Happy path: active filters shown as chips
test('shows active filter chips', async ({ page }) => {
await page.getByRole('checkbox', { name: '{{filterCategory}}' }).check();
await expect(page.getByRole('button', { name: /remove.*{{filterCategory}}/i })).toBeVisible();
});
// Happy path: clear individual filter chip
test('removes filter by clicking chip close', async ({ page }) => {
await page.getByRole('checkbox', { name: '{{filterCategory}}' }).check();
await page.getByRole('button', { name: /remove.*{{filterCategory}}/i }).click();
await expect(page.getByRole('checkbox', { name: '{{filterCategory}}' })).not.toBeChecked();
});
// Happy path: clear all filters
test('clears all filters', async ({ page }) => {
await page.getByRole('checkbox', { name: '{{filterCategory}}' }).check();
await page.getByRole('button', { name: /clear all filters/i }).click();
await expect(page.getByRole('checkbox', { name: '{{filterCategory}}' })).not.toBeChecked();
await expect(page).not.toHaveURL(/category=/);
});
// Error case: no results for filter combination
test('shows empty state when filters yield no results', async ({ page }) => {
await page.getByRole('spinbutton', { name: /min.*price/i }).fill('999999');
await page.getByRole('button', { name: /apply|filter/i }).click();
await expect(page.getByText(/no results/i)).toBeVisible();
await expect(page.getByRole('button', { name: /clear.*filter/i })).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Search Filters', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{searchQuery}}');
});
test('filters results by category', async ({ page }) => {
await page.getByRole('checkbox', { name: '{{filterCategory}}' }).check();
await expect(page).toHaveURL(/category={{filterCategory}}/);
await expect(page.getByRole('listitem').first()).toBeVisible();
});
test('shows active filter chips', async ({ page }) => {
await page.getByRole('checkbox', { name: '{{filterCategory}}' }).check();
await expect(page.getByRole('button', { name: /remove.*{{filterCategory}}/i })).toBeVisible();
});
test('clears all filters', async ({ page }) => {
await page.getByRole('checkbox', { name: '{{filterCategory}}' }).check();
await page.getByRole('button', { name: /clear all filters/i }).click();
await expect(page.getByRole('checkbox', { name: '{{filterCategory}}' })).not.toBeChecked();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Category filter | Checkbox → results scoped to category |
| Price range | Min/max filter applied, prices verified |
| Multi-filter | Multiple checkboxes combine in URL |
| Filter chips | Active filters shown as removable chips |
| Remove chip | Chip close → filter unchecked |
| Clear all | All filters removed at once |
| No-results combo | Filter combination yields empty state |
FILE:templates/search/pagination.md
# Pagination Template
Tests page navigation, items-per-page selector, and URL state.
## Prerequisites
- Search results for `{{searchQuery}}` spanning multiple pages
- At least `{{totalItemCount}}` items total
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Pagination', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{searchQuery}}');
});
// Happy path: navigate to next page
test('navigates to next page and updates URL', async ({ page }) => {
const firstItem = await page.getByRole('listitem').first().textContent();
await page.getByRole('button', { name: /next page/i }).click();
await expect(page).toHaveURL(/page=2/);
await expect(page.getByRole('listitem').first()).not.toHaveText(firstItem!);
});
// Happy path: navigate to previous page
test('navigates to previous page', async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{searchQuery}}&page=2');
const secondPageFirst = await page.getByRole('listitem').first().textContent();
await page.getByRole('button', { name: /previous page/i }).click();
await expect(page).toHaveURL(/page=1/);
await expect(page.getByRole('listitem').first()).not.toHaveText(secondPageFirst!);
});
// Happy path: jump to specific page
test('jumps to specific page number', async ({ page }) => {
await page.getByRole('button', { name: '3' }).click();
await expect(page).toHaveURL(/page=3/);
await expect(page.getByRole('button', { name: '3' })).toHaveAttribute('aria-current', 'page');
});
// Happy path: items per page selector
test('changes items per page', async ({ page }) => {
await page.getByRole('combobox', { name: /per page/i }).selectOption('50');
await expect(page).toHaveURL(/per_page=50/);
const items = page.getByRole('listitem');
await expect(items).toHaveCount(Math.min(50, {{totalItemCount}}));
});
// Happy path: page info text
test('shows correct page info text', async ({ page }) => {
await expect(page.getByText(/showing \d+.+of\s+{{totalItemCount}}/i)).toBeVisible();
});
// Error case: first page has no previous button
test('previous page button disabled on first page', async ({ page }) => {
await expect(page.getByRole('button', { name: /previous page/i })).toBeDisabled();
});
// Error case: last page has no next button
test('next page button disabled on last page', async ({ page }) => {
const lastPage = Math.ceil({{totalItemCount}} / {{defaultPageSize}});
await page.goto(`{{baseUrl}}/search?q={{searchQuery}}&page=lastPage`);
await expect(page.getByRole('button', { name: /next page/i })).toBeDisabled();
});
// Edge case: out-of-range page redirects to last page
test('out-of-range page parameter redirects gracefully', async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{searchQuery}}&page=99999');
await expect(page.getByRole('listitem').first()).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Pagination', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{searchQuery}}');
});
test('navigates to next page', async ({ page }) => {
await page.getByRole('button', { name: /next page/i }).click();
await expect(page).toHaveURL(/page=2/);
});
test('previous page disabled on first page', async ({ page }) => {
await expect(page.getByRole('button', { name: /previous page/i })).toBeDisabled();
});
test('next page disabled on last page', async ({ page }) => {
const last = Math.ceil({{totalItemCount}} / {{defaultPageSize}});
await page.goto(`{{baseUrl}}/search?q={{searchQuery}}&page=last`);
await expect(page.getByRole('button', { name: /next page/i })).toBeDisabled();
});
test('changes items per page', async ({ page }) => {
await page.getByRole('combobox', { name: /per page/i }).selectOption('50');
await expect(page).toHaveURL(/per_page=50/);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Next page | Items change, URL updates page=2 |
| Previous page | Back to page 1 |
| Jump to page | Clicking page number sets aria-current |
| Items per page | Selector changes count of visible items |
| Page info | "Showing X-Y of N" text |
| First page prev | Previous button disabled |
| Last page next | Next button disabled |
| Out-of-range | Graceful fallback |
FILE:templates/search/sorting.md
# Search Sorting Template
Tests sorting results by name, date, and price.
## Prerequisites
- Search results for `{{searchQuery}}` with multiple items
- App running at `{{baseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Search Sorting', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{searchQuery}}');
});
// Happy path: sort by name A-Z
test('sorts results alphabetically A-Z', async ({ page }) => {
await page.getByRole('combobox', { name: /sort by/i }).selectOption('name_asc');
await expect(page).toHaveURL(/sort=name_asc/);
const names = page.getByTestId('result-name');
const first = await names.first().textContent();
const second = await names.nth(1).textContent();
expect(first!.localeCompare(second!)).toBeLessThanOrEqual(0);
});
// Happy path: sort by name Z-A
test('sorts results alphabetically Z-A', async ({ page }) => {
await page.getByRole('combobox', { name: /sort by/i }).selectOption('name_desc');
const names = page.getByTestId('result-name');
const first = await names.first().textContent();
const second = await names.nth(1).textContent();
expect(first!.localeCompare(second!)).toBeGreaterThanOrEqual(0);
});
// Happy path: sort by date newest
test('sorts results by newest date first', async ({ page }) => {
await page.getByRole('combobox', { name: /sort by/i }).selectOption('date_desc');
await expect(page).toHaveURL(/sort=date_desc/);
const dates = page.getByTestId('result-date');
const firstDate = new Date(await dates.first().getAttribute('datetime') ?? '');
const secondDate = new Date(await dates.nth(1).getAttribute('datetime') ?? '');
expect(firstDate.getTime()).toBeGreaterThanOrEqual(secondDate.getTime());
});
// Happy path: sort by price low-high
test('sorts by price low to high', async ({ page }) => {
await page.getByRole('combobox', { name: /sort by/i }).selectOption('price_asc');
const prices = page.getByTestId('result-price');
const firstText = await prices.first().textContent() ?? '';
const secondText = await prices.nth(1).textContent() ?? '';
const first = parseFloat(firstText.replace(/[^0-9.]/g, ''));
const second = parseFloat(secondText.replace(/[^0-9.]/g, ''));
expect(first).toBeLessThanOrEqual(second);
});
// Happy path: sort by price high-low
test('sorts by price high to low', async ({ page }) => {
await page.getByRole('combobox', { name: /sort by/i }).selectOption('price_desc');
const prices = page.getByTestId('result-price');
const firstText = await prices.first().textContent() ?? '';
const secondText = await prices.nth(1).textContent() ?? '';
const first = parseFloat(firstText.replace(/[^0-9.]/g, ''));
const second = parseFloat(secondText.replace(/[^0-9.]/g, ''));
expect(first).toBeGreaterThanOrEqual(second);
});
// Happy path: sort persists with filters
test('sort selection persists when filter applied', async ({ page }) => {
await page.getByRole('combobox', { name: /sort by/i }).selectOption('price_asc');
await page.getByRole('checkbox', { name: '{{filterCategory}}' }).check();
await expect(page).toHaveURL(/sort=price_asc/);
await expect(page.getByRole('combobox', { name: /sort by/i })).toHaveValue('price_asc');
});
// Edge case: default sort is relevance
test('default sort is relevance', async ({ page }) => {
await expect(page.getByRole('combobox', { name: /sort by/i })).toHaveValue('relevance');
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Search Sorting', () => {
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/search?q={{searchQuery}}');
});
test('sorts alphabetically A-Z', async ({ page }) => {
await page.getByRole('combobox', { name: /sort by/i }).selectOption('name_asc');
await expect(page).toHaveURL(/sort=name_asc/);
const names = page.getByTestId('result-name');
const first = await names.first().textContent();
const second = await names.nth(1).textContent();
expect(first.localeCompare(second)).toBeLessThanOrEqual(0);
});
test('sorts by price low to high', async ({ page }) => {
await page.getByRole('combobox', { name: /sort by/i }).selectOption('price_asc');
const prices = page.getByTestId('result-price');
const a = parseFloat((await prices.first().textContent()).replace(/[^0-9.]/g, ''));
const b = parseFloat((await prices.nth(1).textContent()).replace(/[^0-9.]/g, ''));
expect(a).toBeLessThanOrEqual(b);
});
test('default sort is relevance', async ({ page }) => {
await expect(page.getByRole('combobox', { name: /sort by/i })).toHaveValue('relevance');
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Name A-Z | First result ≤ second alphabetically |
| Name Z-A | First result ≥ second alphabetically |
| Date newest | Dates in descending order |
| Price low-high | Prices in ascending order |
| Price high-low | Prices in descending order |
| Sort + filter | Sort param persists when filter applied |
| Default sort | Relevance selected by default |
FILE:templates/settings/account-delete.md
# Account Delete Template
Tests account deletion flow with confirmation and data warning.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Disposable test account (deletion is irreversible)
- Settings at `{{baseUrl}}/settings/account`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Account Delete', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/settings/account');
});
// Happy path: delete button opens confirmation
test('clicking delete account shows confirmation dialog', async ({ page }) => {
await page.getByRole('button', { name: /delete.*account/i }).click();
const dialog = page.getByRole('dialog', { name: /delete account/i });
await expect(dialog).toBeVisible();
await expect(dialog).toContainText(/irreversible|cannot be undone/i);
await expect(dialog).toContainText(/{{dataWarningText}}/i);
});
// Happy path: cancel preserves account
test('cancel keeps account intact', async ({ page }) => {
await page.getByRole('button', { name: /delete.*account/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /cancel/i }).click();
await expect(page.getByRole('dialog')).toBeHidden();
await expect(page).toHaveURL('{{baseUrl}}/settings/account');
});
// Happy path: type-to-confirm gates deletion
test('confirm button disabled until account email typed', async ({ page }) => {
await page.getByRole('button', { name: /delete.*account/i }).click();
const dialog = page.getByRole('dialog');
const confirmBtn = dialog.getByRole('button', { name: /delete.*account|confirm/i });
await expect(confirmBtn).toBeDisabled();
await dialog.getByRole('textbox', { name: /type.*email/i }).fill('{{username}}');
await expect(confirmBtn).toBeEnabled();
});
// Happy path: successful deletion redirects to login
test('deletes account and redirects to login', async ({ page }) => {
await page.getByRole('button', { name: /delete.*account/i }).click();
const dialog = page.getByRole('dialog');
await dialog.getByRole('textbox', { name: /type.*email/i }).fill('{{username}}');
await dialog.getByRole('button', { name: /delete.*account|confirm/i }).click();
await expect(page).toHaveURL(/\/login/);
await expect(page.getByText(/account.*deleted|successfully deleted/i)).toBeVisible();
});
// Error case: wrong email in confirmation box
test('shows error when wrong email typed in confirmation', async ({ page }) => {
await page.getByRole('button', { name: /delete.*account/i }).click();
const dialog = page.getByRole('dialog');
await dialog.getByRole('textbox', { name: /type.*email/i }).fill('[email protected]');
const confirmBtn = dialog.getByRole('button', { name: /delete.*account|confirm/i });
await expect(confirmBtn).toBeDisabled();
await expect(dialog.getByText(/does not match/i)).toBeVisible();
});
// Error case: deletion fails server-side
test('shows error when account deletion fails', async ({ page }) => {
await page.route('{{baseUrl}}/api/account', route =>
route.fulfill({ status: 500, body: JSON.stringify({ error: 'Deletion failed' }) })
);
await page.getByRole('button', { name: /delete.*account/i }).click();
const dialog = page.getByRole('dialog');
await dialog.getByRole('textbox', { name: /type.*email/i }).fill('{{username}}');
await dialog.getByRole('button', { name: /confirm/i }).click();
await expect(page.getByRole('alert')).toContainText(/failed|error/i);
await expect(page).toHaveURL('{{baseUrl}}/settings/account');
});
// Edge case: data export offered before deletion
test('shows data export option in deletion dialog', async ({ page }) => {
await page.getByRole('button', { name: /delete.*account/i }).click();
await expect(page.getByRole('link', { name: /export.*data|download.*data/i })).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Account Delete', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('shows confirmation dialog on delete', async ({ page }) => {
await page.goto('{{baseUrl}}/settings/account');
await page.getByRole('button', { name: /delete.*account/i }).click();
await expect(page.getByRole('dialog', { name: /delete account/i })).toBeVisible();
await expect(page.getByRole('dialog')).toContainText(/irreversible/i);
});
test('confirm button disabled until email typed', async ({ page }) => {
await page.goto('{{baseUrl}}/settings/account');
await page.getByRole('button', { name: /delete.*account/i }).click();
const dialog = page.getByRole('dialog');
await expect(dialog.getByRole('button', { name: /confirm/i })).toBeDisabled();
await dialog.getByRole('textbox', { name: /type.*email/i }).fill('{{username}}');
await expect(dialog.getByRole('button', { name: /confirm/i })).toBeEnabled();
});
test('cancel preserves account', async ({ page }) => {
await page.goto('{{baseUrl}}/settings/account');
await page.getByRole('button', { name: /delete.*account/i }).click();
await page.getByRole('dialog').getByRole('button', { name: /cancel/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/settings/account');
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Dialog opens | Delete button → confirmation with warning |
| Cancel | Dialog closed, account preserved |
| Type-to-confirm | Button enabled only with correct email |
| Successful delete | Account deleted → /login |
| Wrong email | Input mismatch → button stays disabled |
| Server error | Deletion fails → error alert |
| Data export | Export link offered in dialog |
FILE:templates/settings/notification-prefs.md
# Notification Preferences Template
Tests toggling notification channels and saving preferences.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Settings page at `{{baseUrl}}/settings/notifications`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Notification Preferences', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/settings/notifications');
});
// Happy path: enable email notifications
test('enables email notifications', async ({ page }) => {
const emailToggle = page.getByRole('switch', { name: /email notifications/i });
if (!(await emailToggle.isChecked())) {
await emailToggle.click();
}
await expect(emailToggle).toBeChecked();
await page.getByRole('button', { name: /save|update/i }).click();
await expect(page.getByRole('alert')).toContainText(/preferences.*saved|updated/i);
});
// Happy path: disable push notifications
test('disables push notifications', async ({ page }) => {
const pushToggle = page.getByRole('switch', { name: /push notifications/i });
if (await pushToggle.isChecked()) {
await pushToggle.click();
}
await expect(pushToggle).not.toBeChecked();
await page.getByRole('button', { name: /save/i }).click();
await expect(page.getByRole('alert')).toContainText(/saved/i);
});
// Happy path: preferences persist after reload
test('saved preferences persist after page reload', async ({ page }) => {
const emailToggle = page.getByRole('switch', { name: /email notifications/i });
const wasChecked = await emailToggle.isChecked();
await emailToggle.click();
await page.getByRole('button', { name: /save/i }).click();
await expect(page.getByRole('alert')).toContainText(/saved/i);
await page.reload();
if (wasChecked) {
await expect(emailToggle).not.toBeChecked();
} else {
await expect(emailToggle).toBeChecked();
}
});
// Happy path: notification frequency selector
test('changes notification frequency', async ({ page }) => {
await page.getByRole('combobox', { name: /frequency|digest/i }).selectOption('{{frequency}}');
await page.getByRole('button', { name: /save/i }).click();
await expect(page.getByRole('alert')).toContainText(/saved/i);
await page.reload();
await expect(page.getByRole('combobox', { name: /frequency|digest/i })).toHaveValue('{{frequency}}');
});
// Error case: save fails — preferences not changed
test('shows error when save fails', async ({ page }) => {
await page.route('{{baseUrl}}/api/settings/notifications*', route =>
route.fulfill({ status: 500, body: JSON.stringify({ error: 'Server error' }) })
);
await page.getByRole('switch', { name: /email notifications/i }).click();
await page.getByRole('button', { name: /save/i }).click();
await expect(page.getByRole('alert')).toContainText(/error|failed to save/i);
});
// Edge case: unsubscribe all shows confirmation
test('shows confirmation before unsubscribing all', async ({ page }) => {
await page.getByRole('button', { name: /unsubscribe all/i }).click();
await expect(page.getByRole('dialog', { name: /unsubscribe/i })).toBeVisible();
await page.getByRole('button', { name: /cancel/i }).click();
// Still subscribed
await expect(page.getByRole('switch', { name: /email notifications/i })).toBeChecked();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Notification Preferences', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('saves notification preferences', async ({ page }) => {
await page.goto('{{baseUrl}}/settings/notifications');
const toggle = page.getByRole('switch', { name: /email notifications/i });
await toggle.click();
await page.getByRole('button', { name: /save/i }).click();
await expect(page.getByRole('alert')).toContainText(/saved/i);
});
test('preferences persist after reload', async ({ page }) => {
await page.goto('{{baseUrl}}/settings/notifications');
const toggle = page.getByRole('switch', { name: /email notifications/i });
const was = await toggle.isChecked();
await toggle.click();
await page.getByRole('button', { name: /save/i }).click();
await page.reload();
was
? await expect(toggle).not.toBeChecked()
: await expect(toggle).toBeChecked();
});
test('shows error when save fails', async ({ page }) => {
await page.goto('{{baseUrl}}/settings/notifications');
await page.route('{{baseUrl}}/api/settings/notifications*', r =>
r.fulfill({ status: 500, body: '{}' })
);
await page.getByRole('button', { name: /save/i }).click();
await expect(page.getByRole('alert')).toContainText(/error|failed/i);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Enable email | Toggle on → saved → success |
| Disable push | Toggle off → saved |
| Persists reload | Saved state survives page reload |
| Frequency selector | Dropdown value saved and restored |
| Save error | Server error → error alert |
| Unsubscribe all | Confirmation dialog before all disabled |
FILE:templates/settings/password-change.md
# Password Change Template
Tests current password verification, new password validation, and success flow.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Current password: `{{currentPassword}}`
- New password: `{{newPassword}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Password Change', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/settings/security');
});
// Happy path: successful password change
test('changes password with valid inputs', async ({ page }) => {
await page.getByRole('textbox', { name: /current password/i }).fill('{{currentPassword}}');
await page.getByRole('textbox', { name: /^new password$/i }).fill('{{newPassword}}');
await page.getByRole('textbox', { name: /confirm.*password/i }).fill('{{newPassword}}');
await page.getByRole('button', { name: /change.*password|update password/i }).click();
await expect(page.getByRole('alert')).toContainText(/password.*changed|updated successfully/i);
});
// Happy path: can log in with new password
test('new password accepted on next login', async ({ page, context }) => {
// Change password
await page.getByRole('textbox', { name: /current password/i }).fill('{{currentPassword}}');
await page.getByRole('textbox', { name: /^new password$/i }).fill('{{newPassword}}');
await page.getByRole('textbox', { name: /confirm.*password/i }).fill('{{newPassword}}');
await page.getByRole('button', { name: /change.*password/i }).click();
await expect(page.getByRole('alert')).toContainText(/changed/i);
// Log out and back in
await page.getByRole('button', { name: /user menu/i }).click();
await page.getByRole('menuitem', { name: /sign out/i }).click();
await page.getByRole('textbox', { name: /email/i }).fill('{{username}}');
await page.getByRole('textbox', { name: /password/i }).fill('{{newPassword}}');
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page).toHaveURL('{{baseUrl}}/dashboard');
});
// Error case: wrong current password
test('shows error when current password is wrong', async ({ page }) => {
await page.getByRole('textbox', { name: /current password/i }).fill('wrong-password');
await page.getByRole('textbox', { name: /^new password$/i }).fill('{{newPassword}}');
await page.getByRole('textbox', { name: /confirm.*password/i }).fill('{{newPassword}}');
await page.getByRole('button', { name: /change.*password/i }).click();
await expect(page.getByRole('alert')).toContainText(/current password.*incorrect|wrong password/i);
});
// Error case: new passwords do not match
test('shows error when confirmation does not match', async ({ page }) => {
await page.getByRole('textbox', { name: /current password/i }).fill('{{currentPassword}}');
await page.getByRole('textbox', { name: /^new password$/i }).fill('{{newPassword}}');
await page.getByRole('textbox', { name: /confirm.*password/i }).fill('mismatch');
await page.getByRole('button', { name: /change.*password/i }).click();
await expect(page.getByText(/passwords.*do not match/i)).toBeVisible();
});
// Error case: new password too weak
test('shows strength error for weak new password', async ({ page }) => {
await page.getByRole('textbox', { name: /current password/i }).fill('{{currentPassword}}');
await page.getByRole('textbox', { name: /^new password$/i }).fill('123');
await page.getByRole('textbox', { name: /^new password$/i }).blur();
await expect(page.getByText(/too weak|at least \d+ characters/i)).toBeVisible();
});
// Error case: new password same as current
test('shows error when new password matches current', async ({ page }) => {
await page.getByRole('textbox', { name: /current password/i }).fill('{{currentPassword}}');
await page.getByRole('textbox', { name: /^new password$/i }).fill('{{currentPassword}}');
await page.getByRole('textbox', { name: /confirm.*password/i }).fill('{{currentPassword}}');
await page.getByRole('button', { name: /change.*password/i }).click();
await expect(page.getByText(/same as.*current|choose.*different/i)).toBeVisible();
});
// Edge case: password strength meter updates on input
test('strength meter reacts to new password input', async ({ page }) => {
await page.getByRole('textbox', { name: /^new password$/i }).fill('weak');
await expect(page.getByRole('meter', { name: /strength/i })).toHaveAttribute('aria-valuenow', '1');
await page.getByRole('textbox', { name: /^new password$/i }).fill('Str0ng!Pass#2026');
await expect(page.getByRole('meter', { name: /strength/i })).toHaveAttribute('aria-valuenow', '4');
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Password Change', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('changes password with valid inputs', async ({ page }) => {
await page.goto('{{baseUrl}}/settings/security');
await page.getByRole('textbox', { name: /current password/i }).fill('{{currentPassword}}');
await page.getByRole('textbox', { name: /^new password$/i }).fill('{{newPassword}}');
await page.getByRole('textbox', { name: /confirm.*password/i }).fill('{{newPassword}}');
await page.getByRole('button', { name: /change.*password/i }).click();
await expect(page.getByRole('alert')).toContainText(/changed|updated/i);
});
test('shows error for wrong current password', async ({ page }) => {
await page.goto('{{baseUrl}}/settings/security');
await page.getByRole('textbox', { name: /current password/i }).fill('wrong');
await page.getByRole('textbox', { name: /^new password$/i }).fill('{{newPassword}}');
await page.getByRole('textbox', { name: /confirm.*password/i }).fill('{{newPassword}}');
await page.getByRole('button', { name: /change.*password/i }).click();
await expect(page.getByRole('alert')).toContainText(/incorrect|wrong/i);
});
test('shows mismatch error', async ({ page }) => {
await page.goto('{{baseUrl}}/settings/security');
await page.getByRole('textbox', { name: /current password/i }).fill('{{currentPassword}}');
await page.getByRole('textbox', { name: /^new password$/i }).fill('{{newPassword}}');
await page.getByRole('textbox', { name: /confirm.*password/i }).fill('nope');
await page.getByRole('button', { name: /change.*password/i }).click();
await expect(page.getByText(/do not match/i)).toBeVisible();
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Success | All fields valid → success alert |
| Login with new pw | New password accepted at login |
| Wrong current | Incorrect current → error alert |
| Mismatch | Confirm ≠ new → validation error |
| Weak password | Short password → strength error |
| Same as current | Reuse blocked with error |
| Strength meter | Meter aria-valuenow updates on input |
FILE:templates/settings/profile-update.md
# Profile Update Template
Tests updating name, email, and avatar in user profile settings.
## Prerequisites
- Authenticated session via `{{authStorageStatePath}}`
- Current name: `{{currentName}}`, email: `{{currentEmail}}`
- Test avatar image: `{{avatarFilePath}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
test.describe('Profile Update', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test.beforeEach(async ({ page }) => {
await page.goto('{{baseUrl}}/settings/profile');
});
// Happy path: update display name
test('updates display name', async ({ page }) => {
const nameField = page.getByRole('textbox', { name: /display name|full name/i });
await nameField.clear();
await nameField.fill('{{newName}}');
await page.getByRole('button', { name: /save|update/i }).click();
await expect(page.getByRole('alert')).toContainText(/profile updated|saved/i);
await expect(page.getByRole('textbox', { name: /display name|full name/i })).toHaveValue('{{newName}}');
});
// Happy path: update email
test('updates email address', async ({ page }) => {
const emailField = page.getByRole('textbox', { name: /email/i });
await emailField.clear();
await emailField.fill('{{newEmail}}');
await page.getByRole('button', { name: /save|update/i }).click();
await expect(page.getByRole('alert')).toContainText(/verification.*sent|email updated/i);
});
// Happy path: upload avatar
test('uploads new avatar image', async ({ page }) => {
await page.getByRole('button', { name: /change.*avatar|upload.*photo/i }).click();
await page.locator('input[type="file"]').setInputFiles('{{avatarFilePath}}');
await expect(page.getByRole('img', { name: /avatar preview/i })).toBeVisible();
await page.getByRole('button', { name: /save|apply/i }).click();
await expect(page.getByRole('alert')).toContainText(/avatar updated|photo saved/i);
});
// Happy path: avatar crop dialog
test('shows crop dialog after avatar upload', async ({ page }) => {
await page.locator('input[type="file"]').setInputFiles('{{avatarFilePath}}');
await expect(page.getByRole('dialog', { name: /crop/i })).toBeVisible();
await page.getByRole('button', { name: /apply crop/i }).click();
await expect(page.getByRole('dialog', { name: /crop/i })).toBeHidden();
});
// Error case: invalid email format
test('shows error for invalid email format', async ({ page }) => {
await page.getByRole('textbox', { name: /email/i }).clear();
await page.getByRole('textbox', { name: /email/i }).fill('bad-email');
await page.getByRole('button', { name: /save|update/i }).click();
await expect(page.getByText(/valid.*email/i)).toBeVisible();
});
// Error case: email already taken
test('shows error when email is already in use', async ({ page }) => {
await page.getByRole('textbox', { name: /email/i }).clear();
await page.getByRole('textbox', { name: /email/i }).fill('{{takenEmail}}');
await page.getByRole('button', { name: /save|update/i }).click();
await expect(page.getByRole('alert')).toContainText(/already in use|taken/i);
});
// Edge case: name reflected in nav after update
test('nav shows updated name after save', async ({ page }) => {
const nameField = page.getByRole('textbox', { name: /display name|full name/i });
await nameField.clear();
await nameField.fill('{{newName}}');
await page.getByRole('button', { name: /save|update/i }).click();
await expect(page.getByRole('navigation').getByText('{{newName}}')).toBeVisible();
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
test.describe('Profile Update', () => {
test.use({ storageState: '{{authStorageStatePath}}' });
test('updates display name', async ({ page }) => {
await page.goto('{{baseUrl}}/settings/profile');
await page.getByRole('textbox', { name: /display name|full name/i }).clear();
await page.getByRole('textbox', { name: /display name|full name/i }).fill('{{newName}}');
await page.getByRole('button', { name: /save|update/i }).click();
await expect(page.getByRole('alert')).toContainText(/profile updated|saved/i);
});
test('shows error for invalid email', async ({ page }) => {
await page.goto('{{baseUrl}}/settings/profile');
await page.getByRole('textbox', { name: /email/i }).fill('bad-email');
await page.getByRole('button', { name: /save|update/i }).click();
await expect(page.getByText(/valid.*email/i)).toBeVisible();
});
test('uploads avatar image', async ({ page }) => {
await page.goto('{{baseUrl}}/settings/profile');
await page.locator('input[type="file"]').setInputFiles('{{avatarFilePath}}');
await page.getByRole('button', { name: /save|apply/i }).click();
await expect(page.getByRole('alert')).toContainText(/avatar updated/i);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Name update | Name saved, field reflects new value |
| Email update | Email saved, verification notice shown |
| Avatar upload | Image uploaded, success alert |
| Crop dialog | Cropper shown, apply saves |
| Invalid email | Format error shown |
| Taken email | Duplicate error shown |
| Nav update | Navigation reflects new name |
Code review automation for TypeScript, JavaScript, Python, Go, Swift, Kotlin. Analyzes PRs for complexity and risk, checks code quality for SOLID violations...
---
name: code-reviewer
description: Code review automation for TypeScript, JavaScript, Python, Go, Swift, Kotlin. Analyzes PRs for complexity and risk, checks code quality for SOLID violations and code smells, generates review reports. Use when reviewing pull requests, analyzing code quality, identifying issues, generating review checklists.
---
# Code Reviewer
Automated code review tools for analyzing pull requests, detecting code quality issues, and generating review reports.
---
## Table of Contents
- [Tools](#tools)
- [PR Analyzer](#pr-analyzer)
- [Code Quality Checker](#code-quality-checker)
- [Review Report Generator](#review-report-generator)
- [Reference Guides](#reference-guides)
- [Languages Supported](#languages-supported)
---
## Tools
### PR Analyzer
Analyzes git diff between branches to assess review complexity and identify risks.
```bash
# Analyze current branch against main
python scripts/pr_analyzer.py /path/to/repo
# Compare specific branches
python scripts/pr_analyzer.py . --base main --head feature-branch
# JSON output for integration
python scripts/pr_analyzer.py /path/to/repo --json
```
**What it detects:**
- Hardcoded secrets (passwords, API keys, tokens)
- SQL injection patterns (string concatenation in queries)
- Debug statements (debugger, console.log)
- ESLint rule disabling
- TypeScript `any` types
- TODO/FIXME comments
**Output includes:**
- Complexity score (1-10)
- Risk categorization (critical, high, medium, low)
- File prioritization for review order
- Commit message validation
---
### Code Quality Checker
Analyzes source code for structural issues, code smells, and SOLID violations.
```bash
# Analyze a directory
python scripts/code_quality_checker.py /path/to/code
# Analyze specific language
python scripts/code_quality_checker.py . --language python
# JSON output
python scripts/code_quality_checker.py /path/to/code --json
```
**What it detects:**
- Long functions (>50 lines)
- Large files (>500 lines)
- God classes (>20 methods)
- Deep nesting (>4 levels)
- Too many parameters (>5)
- High cyclomatic complexity
- Missing error handling
- Unused imports
- Magic numbers
**Thresholds:**
| Issue | Threshold |
|-------|-----------|
| Long function | >50 lines |
| Large file | >500 lines |
| God class | >20 methods |
| Too many params | >5 |
| Deep nesting | >4 levels |
| High complexity | >10 branches |
---
### Review Report Generator
Combines PR analysis and code quality findings into structured review reports.
```bash
# Generate report for current repo
python scripts/review_report_generator.py /path/to/repo
# Markdown output
python scripts/review_report_generator.py . --format markdown --output review.md
# Use pre-computed analyses
python scripts/review_report_generator.py . \
--pr-analysis pr_results.json \
--quality-analysis quality_results.json
```
**Report includes:**
- Review verdict (approve, request changes, block)
- Score (0-100)
- Prioritized action items
- Issue summary by severity
- Suggested review order
**Verdicts:**
| Score | Verdict |
|-------|---------|
| 90+ with no high issues | Approve |
| 75+ with ≤2 high issues | Approve with suggestions |
| 50-74 | Request changes |
| <50 or critical issues | Block |
---
## Reference Guides
### Code Review Checklist
`references/code_review_checklist.md`
Systematic checklists covering:
- Pre-review checks (build, tests, PR hygiene)
- Correctness (logic, data handling, error handling)
- Security (input validation, injection prevention)
- Performance (efficiency, caching, scalability)
- Maintainability (code quality, naming, structure)
- Testing (coverage, quality, mocking)
- Language-specific checks
### Coding Standards
`references/coding_standards.md`
Language-specific standards for:
- TypeScript (type annotations, null safety, async/await)
- JavaScript (declarations, patterns, modules)
- Python (type hints, exceptions, class design)
- Go (error handling, structs, concurrency)
- Swift (optionals, protocols, errors)
- Kotlin (null safety, data classes, coroutines)
### Common Antipatterns
`references/common_antipatterns.md`
Antipattern catalog with examples and fixes:
- Structural (god class, long method, deep nesting)
- Logic (boolean blindness, stringly typed code)
- Security (SQL injection, hardcoded credentials)
- Performance (N+1 queries, unbounded collections)
- Testing (duplication, testing implementation)
- Async (floating promises, callback hell)
---
## Languages Supported
| Language | Extensions |
|----------|------------|
| Python | `.py` |
| TypeScript | `.ts`, `.tsx` |
| JavaScript | `.js`, `.jsx`, `.mjs` |
| Go | `.go` |
| Swift | `.swift` |
| Kotlin | `.kt`, `.kts` |
FILE:references/code_review_checklist.md
# Code Review Checklist
Structured checklists for systematic code review across different aspects.
---
## Table of Contents
- [Pre-Review Checks](#pre-review-checks)
- [Correctness](#correctness)
- [Security](#security)
- [Performance](#performance)
- [Maintainability](#maintainability)
- [Testing](#testing)
- [Documentation](#documentation)
- [Language-Specific Checks](#language-specific-checks)
---
## Pre-Review Checks
Before diving into code, verify these basics:
### Build and Tests
- [ ] Code compiles without errors
- [ ] All existing tests pass
- [ ] New tests are included for new functionality
- [ ] No unintended files included (build artifacts, IDE configs)
### PR Hygiene
- [ ] PR has clear title and description
- [ ] Changes are scoped appropriately (not too large)
- [ ] Commits follow conventional commit format
- [ ] Branch is up to date with base branch
### Scope Verification
- [ ] Changes match the stated purpose
- [ ] No unrelated changes bundled in
- [ ] Breaking changes are documented
- [ ] Migration path provided if needed
---
## Correctness
### Logic
- [ ] Algorithm implements requirements correctly
- [ ] Edge cases handled (null, empty, boundary values)
- [ ] Off-by-one errors checked
- [ ] Correct operators used (== vs ===, & vs &&)
- [ ] Loop termination conditions correct
- [ ] Recursion has proper base cases
### Data Handling
- [ ] Data types appropriate for the use case
- [ ] Numeric overflow/underflow considered
- [ ] Date/time handling accounts for timezones
- [ ] Unicode and internationalization handled
- [ ] Data validation at entry points
### State Management
- [ ] State transitions are valid
- [ ] Race conditions addressed
- [ ] Concurrent access handled correctly
- [ ] State cleanup on errors/exit
### Error Handling
- [ ] Errors caught at appropriate levels
- [ ] Error messages are actionable
- [ ] Errors don't expose sensitive information
- [ ] Recovery or graceful degradation implemented
- [ ] Resources cleaned up in error paths
---
## Security
### Input Validation
- [ ] All user input validated and sanitized
- [ ] Input length limits enforced
- [ ] File uploads validated (type, size, content)
- [ ] URL parameters validated
### Injection Prevention
- [ ] SQL queries parameterized
- [ ] Command execution uses safe APIs
- [ ] HTML output escaped to prevent XSS
- [ ] LDAP queries properly escaped
- [ ] XML parsing disables external entities
### Authentication & Authorization
- [ ] Authentication required for protected resources
- [ ] Authorization checked before operations
- [ ] Session management secure
- [ ] Password handling follows best practices
- [ ] Token expiration implemented
### Data Protection
- [ ] Sensitive data encrypted at rest
- [ ] Sensitive data encrypted in transit
- [ ] PII handled according to policy
- [ ] Secrets not hardcoded
- [ ] Logs don't contain sensitive data
### API Security
- [ ] Rate limiting implemented
- [ ] CORS configured correctly
- [ ] CSRF protection in place
- [ ] API keys/tokens secured
- [ ] Endpoints use HTTPS
---
## Performance
### Efficiency
- [ ] Appropriate data structures used
- [ ] Algorithms have acceptable complexity
- [ ] Database queries are optimized
- [ ] N+1 query problems avoided
- [ ] Indexes used where beneficial
### Resource Usage
- [ ] Memory usage bounded
- [ ] No memory leaks
- [ ] File handles properly closed
- [ ] Database connections pooled
- [ ] Network calls minimized
### Caching
- [ ] Appropriate caching strategy
- [ ] Cache invalidation handled
- [ ] Cache keys are unique and predictable
- [ ] TTL values appropriate
### Scalability
- [ ] Horizontal scaling considered
- [ ] Bottlenecks identified
- [ ] Async processing for long operations
- [ ] Batch operations where appropriate
---
## Maintainability
### Code Quality
- [ ] Functions/methods have single responsibility
- [ ] Classes follow SOLID principles
- [ ] Code is DRY (Don't Repeat Yourself)
- [ ] No dead code or commented-out code
- [ ] Magic numbers replaced with constants
### Naming
- [ ] Names are descriptive and consistent
- [ ] Naming follows project conventions
- [ ] No abbreviations that obscure meaning
- [ ] Boolean variables/functions have is/has/can prefix
### Structure
- [ ] Functions are appropriately sized (<50 lines preferred)
- [ ] Nesting depth is reasonable (<4 levels)
- [ ] Related code is grouped together
- [ ] Dependencies are minimal and explicit
### Readability
- [ ] Code is self-documenting where possible
- [ ] Complex logic has explanatory comments
- [ ] Formatting is consistent
- [ ] No overly clever or obscure code
---
## Testing
### Coverage
- [ ] New code has unit tests
- [ ] Critical paths have integration tests
- [ ] Edge cases are tested
- [ ] Error conditions are tested
### Quality
- [ ] Tests are independent
- [ ] Tests have clear assertions
- [ ] Test names describe what is tested
- [ ] Tests don't depend on external state
### Mocking
- [ ] External dependencies are mocked
- [ ] Mocks are realistic
- [ ] Mock setup is not excessive
---
## Documentation
### Code Documentation
- [ ] Public APIs are documented
- [ ] Complex algorithms explained
- [ ] Non-obvious decisions documented
- [ ] TODO/FIXME comments have context
### External Documentation
- [ ] README updated if needed
- [ ] API documentation updated
- [ ] Changelog updated
- [ ] Migration guides provided
---
## Language-Specific Checks
### TypeScript/JavaScript
- [ ] Types are explicit (avoid `any`)
- [ ] Null checks present (`?.`, `??`)
- [ ] Async/await errors handled
- [ ] No floating promises
- [ ] Memory leaks from closures checked
### Python
- [ ] Type hints used for public APIs
- [ ] Context managers for resources (`with` statements)
- [ ] Exception handling is specific (not bare `except`)
- [ ] No mutable default arguments
- [ ] List comprehensions used appropriately
### Go
- [ ] Errors checked and handled
- [ ] Goroutine leaks prevented
- [ ] Context propagation correct
- [ ] Defer statements in right order
- [ ] Interfaces minimal
### Swift
- [ ] Optionals handled safely
- [ ] Memory management correct (weak/unowned)
- [ ] Error handling uses Result or throws
- [ ] Access control appropriate
- [ ] Codable implementation correct
### Kotlin
- [ ] Null safety leveraged
- [ ] Coroutine cancellation handled
- [ ] Data classes used appropriately
- [ ] Extension functions don't obscure behavior
- [ ] Sealed classes for state
---
## Review Process Tips
### Before Approving
1. Verify all critical checks passed
2. Confirm tests are adequate
3. Consider deployment impact
4. Check for any security concerns
5. Ensure documentation is updated
### Providing Feedback
- Be specific about issues
- Explain why something is problematic
- Suggest alternatives when possible
- Distinguish blockers from suggestions
- Acknowledge good patterns
### When to Block
- Security vulnerabilities present
- Critical logic errors
- No tests for risky changes
- Breaking changes without migration
- Significant performance regressions
FILE:references/coding_standards.md
# Coding Standards
Language-specific coding standards and conventions for code review.
---
## Table of Contents
- [Universal Principles](#universal-principles)
- [TypeScript Standards](#typescript-standards)
- [JavaScript Standards](#javascript-standards)
- [Python Standards](#python-standards)
- [Go Standards](#go-standards)
- [Swift Standards](#swift-standards)
- [Kotlin Standards](#kotlin-standards)
---
## Universal Principles
These apply across all languages.
### Naming Conventions
| Element | Convention | Example |
|---------|------------|---------|
| Variables | camelCase (JS/TS), snake_case (Python/Go) | `userName`, `user_name` |
| Constants | SCREAMING_SNAKE_CASE | `MAX_RETRY_COUNT` |
| Functions | camelCase (JS/TS), snake_case (Python) | `getUserById`, `get_user_by_id` |
| Classes | PascalCase | `UserRepository` |
| Interfaces | PascalCase, optionally prefixed | `IUserService` or `UserService` |
| Private members | Prefix with underscore or use access modifiers | `_internalState` |
### Function Design
```
Good functions:
- Do one thing well
- Have descriptive names (verb + noun)
- Take 3 or fewer parameters
- Return early for error cases
- Stay under 50 lines
```
### Error Handling
```
Good error handling:
- Catch specific errors, not generic exceptions
- Log with context (what, where, why)
- Clean up resources in error paths
- Don't swallow errors silently
- Provide actionable error messages
```
---
## TypeScript Standards
### Type Annotations
```typescript
// Avoid 'any' - use unknown for truly unknown types
function processData(data: unknown): ProcessedResult {
if (isValidData(data)) {
return transform(data);
}
throw new Error('Invalid data format');
}
// Use explicit return types for public APIs
export function calculateTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Use type guards for runtime checks
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'email' in obj
);
}
```
### Null Safety
```typescript
// Use optional chaining and nullish coalescing
const userName = user?.profile?.name ?? 'Anonymous';
// Be explicit about nullable types
interface Config {
timeout: number;
retries?: number; // Optional
fallbackUrl: string | null; // Explicitly nullable
}
// Use assertion functions for validation
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error('Value is not defined');
}
}
```
### Async/Await
```typescript
// Always handle errors in async functions
async function fetchUser(id: string): Promise<User> {
try {
const response = await api.get(`/users/id`);
return response.data;
} catch (error) {
logger.error('Failed to fetch user', { id, error });
throw new UserFetchError(id, error);
}
}
// Use Promise.all for parallel operations
async function loadDashboard(userId: string): Promise<Dashboard> {
const [profile, stats, notifications] = await Promise.all([
fetchProfile(userId),
fetchStats(userId),
fetchNotifications(userId)
]);
return { profile, stats, notifications };
}
```
### React/Component Standards
```typescript
// Use explicit prop types
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
// Prefer functional components with hooks
function Button({ label, onClick, variant = 'primary', disabled = false }: ButtonProps) {
return (
<button
className={`btn btn-variant`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
}
// Use custom hooks for reusable logic
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
```
---
## JavaScript Standards
### Variable Declarations
```javascript
// Use const by default, let when reassignment needed
const MAX_ITEMS = 100;
let currentCount = 0;
// Never use var
// var is function-scoped and hoisted, leading to bugs
```
### Object and Array Patterns
```javascript
// Use object destructuring
const { name, email, role = 'user' } = user;
// Use spread for immutable updates
const updatedUser = { ...user, lastLogin: new Date() };
const updatedList = [...items, newItem];
// Use array methods over loops
const activeUsers = users.filter(u => u.isActive);
const emails = users.map(u => u.email);
const total = orders.reduce((sum, o) => sum + o.amount, 0);
```
### Module Patterns
```javascript
// Use named exports for utilities
export function formatDate(date) { ... }
export function parseDate(str) { ... }
// Use default export for main component/class
export default class UserService { ... }
// Group related exports
export { formatDate, parseDate, isValidDate } from './dateUtils';
```
---
## Python Standards
### Type Hints (PEP 484)
```python
from typing import Optional, List, Dict, Union
def get_user(user_id: int) -> Optional[User]:
"""Fetch user by ID, returns None if not found."""
return db.query(User).filter(User.id == user_id).first()
def process_items(items: List[str]) -> Dict[str, int]:
"""Count occurrences of each item."""
return {item: items.count(item) for item in set(items)}
def send_notification(
user: User,
message: str,
*,
priority: str = "normal",
channels: List[str] = None
) -> bool:
"""Send notification to user via specified channels."""
channels = channels or ["email"]
# Implementation
```
### Exception Handling
```python
# Catch specific exceptions
try:
result = api_client.fetch_data(endpoint)
except ConnectionError as e:
logger.warning(f"Connection failed: {e}")
return cached_data
except TimeoutError as e:
logger.error(f"Request timed out: {e}")
raise ServiceUnavailableError() from e
# Use context managers for resources
with open(filepath, 'r') as f:
data = json.load(f)
# Custom exceptions should be informative
class ValidationError(Exception):
def __init__(self, field: str, message: str):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
```
### Class Design
```python
from dataclasses import dataclass
from abc import ABC, abstractmethod
# Use dataclasses for data containers
@dataclass
class UserDTO:
id: int
email: str
name: str
is_active: bool = True
# Use ABC for interfaces
class Repository(ABC):
@abstractmethod
def find_by_id(self, id: int) -> Optional[Entity]:
pass
@abstractmethod
def save(self, entity: Entity) -> Entity:
pass
# Use properties for computed attributes
class Order:
def __init__(self, items: List[OrderItem]):
self._items = items
@property
def total(self) -> Decimal:
return sum(item.price * item.quantity for item in self._items)
```
---
## Go Standards
### Error Handling
```go
// Always check errors
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to open %s: %w", filename, err)
}
defer file.Close()
// Use custom error types for specific cases
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// Wrap errors with context
if err := db.Query(query); err != nil {
return fmt.Errorf("query failed for user %d: %w", userID, err)
}
```
### Struct Design
```go
// Use unexported fields with exported methods
type UserService struct {
repo UserRepository
cache Cache
logger Logger
}
// Constructor functions for initialization
func NewUserService(repo UserRepository, cache Cache, logger Logger) *UserService {
return &UserService{
repo: repo,
cache: cache,
logger: logger,
}
}
// Keep interfaces small
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
```
### Concurrency
```go
// Use context for cancellation
func fetchData(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
// ...
}
// Use channels for communication
func worker(jobs <-chan Job, results chan<- Result) {
for job := range jobs {
result := process(job)
results <- result
}
}
// Use sync.WaitGroup for coordination
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(i Item) {
defer wg.Done()
processItem(i)
}(item)
}
wg.Wait()
```
---
## Swift Standards
### Optionals
```swift
// Use optional binding
if let user = fetchUser(id: userId) {
displayProfile(user)
}
// Use guard for early exit
guard let data = response.data else {
throw NetworkError.noData
}
// Use nil coalescing for defaults
let displayName = user.nickname ?? user.email
// Avoid force unwrapping except in tests
// BAD: let name = user.name!
// GOOD: guard let name = user.name else { return }
```
### Protocol-Oriented Design
```swift
// Define protocols with minimal requirements
protocol Identifiable {
var id: String { get }
}
protocol Persistable: Identifiable {
func save() throws
static func find(by id: String) -> Self?
}
// Use protocol extensions for default implementations
extension Persistable {
func save() throws {
try Storage.shared.save(self)
}
}
// Prefer composition over inheritance
struct User: Identifiable, Codable {
let id: String
var name: String
var email: String
}
```
### Error Handling
```swift
// Define domain-specific errors
enum AuthError: Error {
case invalidCredentials
case tokenExpired
case networkFailure(underlying: Error)
}
// Use Result type for async operations
func authenticate(
email: String,
password: String,
completion: @escaping (Result<User, AuthError>) -> Void
)
// Use throws for synchronous operations
func validate(_ input: String) throws -> ValidatedInput {
guard !input.isEmpty else {
throw ValidationError.emptyInput
}
return ValidatedInput(value: input)
}
```
---
## Kotlin Standards
### Null Safety
```kotlin
// Use nullable types explicitly
fun findUser(id: Int): User? {
return userRepository.find(id)
}
// Use safe calls and elvis operator
val name = user?.profile?.name ?: "Unknown"
// Use let for null checks with side effects
user?.let { activeUser ->
sendWelcomeEmail(activeUser.email)
logActivity(activeUser.id)
}
// Use require/check for validation
fun processPayment(amount: Double) {
require(amount > 0) { "Amount must be positive: $amount" }
// Process
}
```
### Data Classes and Sealed Classes
```kotlin
// Use data classes for DTOs
data class UserDTO(
val id: Int,
val email: String,
val name: String,
val isActive: Boolean = true
)
// Use sealed classes for state
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String, val cause: Throwable? = null) : Result<Nothing>()
object Loading : Result<Nothing>()
}
// Pattern matching with when
fun handleResult(result: Result<User>) = when (result) {
is Result.Success -> showUser(result.data)
is Result.Error -> showError(result.message)
Result.Loading -> showLoading()
}
```
### Coroutines
```kotlin
// Use structured concurrency
suspend fun loadDashboard(): Dashboard = coroutineScope {
val profile = async { fetchProfile() }
val stats = async { fetchStats() }
val notifications = async { fetchNotifications() }
Dashboard(
profile = profile.await(),
stats = stats.await(),
notifications = notifications.await()
)
}
// Handle cancellation
suspend fun fetchWithRetry(url: String): Response {
repeat(3) { attempt ->
try {
return httpClient.get(url)
} catch (e: IOException) {
if (attempt == 2) throw e
delay(1000L * (attempt + 1))
}
}
throw IllegalStateException("Unreachable")
}
```
FILE:references/common_antipatterns.md
# Common Antipatterns
Code antipatterns to identify during review, with examples and fixes.
---
## Table of Contents
- [Structural Antipatterns](#structural-antipatterns)
- [Logic Antipatterns](#logic-antipatterns)
- [Security Antipatterns](#security-antipatterns)
- [Performance Antipatterns](#performance-antipatterns)
- [Testing Antipatterns](#testing-antipatterns)
- [Async Antipatterns](#async-antipatterns)
---
## Structural Antipatterns
### God Class
A class that does too much and knows too much.
```typescript
// BAD: God class handling everything
class UserManager {
createUser(data: UserData) { ... }
updateUser(id: string, data: UserData) { ... }
deleteUser(id: string) { ... }
sendEmail(userId: string, content: string) { ... }
generateReport(userId: string) { ... }
validatePassword(password: string) { ... }
hashPassword(password: string) { ... }
uploadAvatar(userId: string, file: File) { ... }
resizeImage(file: File) { ... }
logActivity(userId: string, action: string) { ... }
// 50 more methods...
}
// GOOD: Single responsibility classes
class UserRepository {
create(data: UserData): User { ... }
update(id: string, data: Partial<UserData>): User { ... }
delete(id: string): void { ... }
}
class EmailService {
send(to: string, content: string): void { ... }
}
class PasswordService {
validate(password: string): ValidationResult { ... }
hash(password: string): string { ... }
}
```
**Detection:** Class has >20 methods, >500 lines, or handles unrelated concerns.
---
### Long Method
Functions that do too much and are hard to understand.
```python
# BAD: Long method doing everything
def process_order(order_data):
# Validate order (20 lines)
if not order_data.get('items'):
raise ValueError('No items')
if not order_data.get('customer_id'):
raise ValueError('No customer')
# ... more validation
# Calculate totals (30 lines)
subtotal = 0
for item in order_data['items']:
price = get_product_price(item['product_id'])
subtotal += price * item['quantity']
# ... tax calculation, discounts
# Process payment (40 lines)
payment_result = payment_gateway.charge(...)
# ... handle payment errors
# Create order record (20 lines)
order = Order.create(...)
# Send notifications (20 lines)
send_order_confirmation(...)
notify_warehouse(...)
return order
# GOOD: Composed of focused functions
def process_order(order_data):
validate_order(order_data)
totals = calculate_order_totals(order_data)
payment = process_payment(order_data['customer_id'], totals)
order = create_order_record(order_data, totals, payment)
send_order_notifications(order)
return order
```
**Detection:** Function >50 lines or requires scrolling to read.
---
### Deep Nesting
Excessive indentation making code hard to follow.
```javascript
// BAD: Deep nesting
function processData(data) {
if (data) {
if (data.items) {
if (data.items.length > 0) {
for (const item of data.items) {
if (item.isValid) {
if (item.type === 'premium') {
if (item.price > 100) {
// Finally do something
processItem(item);
}
}
}
}
}
}
}
}
// GOOD: Early returns and guard clauses
function processData(data) {
if (!data?.items?.length) {
return;
}
const premiumItems = data.items.filter(
item => item.isValid && item.type === 'premium' && item.price > 100
);
premiumItems.forEach(processItem);
}
```
**Detection:** Indentation >4 levels deep.
---
### Magic Numbers and Strings
Hard-coded values without explanation.
```go
// BAD: Magic numbers
func calculateDiscount(total float64, userType int) float64 {
if userType == 1 {
return total * 0.15
} else if userType == 2 {
return total * 0.25
}
return total * 0.05
}
// GOOD: Named constants
const (
UserTypeRegular = 1
UserTypePremium = 2
DiscountRegular = 0.05
DiscountStandard = 0.15
DiscountPremium = 0.25
)
func calculateDiscount(total float64, userType int) float64 {
switch userType {
case UserTypePremium:
return total * DiscountPremium
case UserTypeRegular:
return total * DiscountStandard
default:
return total * DiscountRegular
}
}
```
**Detection:** Literal numbers (except 0, 1) or repeated string literals.
---
### Primitive Obsession
Using primitives instead of small objects.
```typescript
// BAD: Primitives everywhere
function createUser(
name: string,
email: string,
phone: string,
street: string,
city: string,
zipCode: string,
country: string
): User { ... }
// GOOD: Value objects
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface ContactInfo {
email: string;
phone: string;
}
function createUser(
name: string,
contact: ContactInfo,
address: Address
): User { ... }
```
**Detection:** Functions with >4 parameters of same type, or related primitives always passed together.
---
## Logic Antipatterns
### Boolean Blindness
Passing booleans that make code unreadable at call sites.
```swift
// BAD: What do these booleans mean?
user.configure(true, false, true, false)
// GOOD: Named parameters or option objects
user.configure(
sendWelcomeEmail: true,
requireVerification: false,
enableNotifications: true,
isAdmin: false
)
// Or use an options struct
struct UserConfiguration {
var sendWelcomeEmail: Bool = true
var requireVerification: Bool = false
var enableNotifications: Bool = true
var isAdmin: Bool = false
}
user.configure(UserConfiguration())
```
**Detection:** Function calls with multiple boolean literals.
---
### Null Returns for Collections
Returning null instead of empty collections.
```kotlin
// BAD: Returning null
fun findUsersByRole(role: String): List<User>? {
val users = repository.findByRole(role)
return if (users.isEmpty()) null else users
}
// Caller must handle null
val users = findUsersByRole("admin")
if (users != null) {
users.forEach { ... }
}
// GOOD: Return empty collection
fun findUsersByRole(role: String): List<User> {
return repository.findByRole(role)
}
// Caller can iterate directly
findUsersByRole("admin").forEach { ... }
```
**Detection:** Functions returning nullable collections.
---
### Stringly Typed Code
Using strings where enums or types should be used.
```python
# BAD: String-based logic
def handle_event(event_type: str, data: dict):
if event_type == "user_created":
handle_user_created(data)
elif event_type == "user_updated":
handle_user_updated(data)
elif event_type == "user_dleted": # Typo won't be caught
handle_user_deleted(data)
# GOOD: Enum-based
from enum import Enum
class EventType(Enum):
USER_CREATED = "user_created"
USER_UPDATED = "user_updated"
USER_DELETED = "user_deleted"
def handle_event(event_type: EventType, data: dict):
handlers = {
EventType.USER_CREATED: handle_user_created,
EventType.USER_UPDATED: handle_user_updated,
EventType.USER_DELETED: handle_user_deleted,
}
handlers[event_type](data)
```
**Detection:** String comparisons for type/status/category values.
---
## Security Antipatterns
### SQL Injection
String concatenation in SQL queries.
```javascript
// BAD: String concatenation
const query = `SELECT * FROM users WHERE id = userId`;
db.query(query);
// BAD: String templates still vulnerable
const query = `SELECT * FROM users WHERE name = 'userName'`;
// GOOD: Parameterized queries
const query = 'SELECT * FROM users WHERE id = $1';
db.query(query, [userId]);
// GOOD: Using ORM safely
User.findOne({ where: { id: userId } });
```
**Detection:** String concatenation or template literals with SQL keywords.
---
### Hardcoded Credentials
Secrets in source code.
```python
# BAD: Hardcoded secrets
API_KEY = "sk-abc123xyz789"
DATABASE_URL = "postgresql://admin:[email protected]:5432/app"
# GOOD: Environment variables
import os
API_KEY = os.environ["API_KEY"]
DATABASE_URL = os.environ["DATABASE_URL"]
# GOOD: Secrets manager
from aws_secretsmanager import get_secret
API_KEY = get_secret("api-key")
```
**Detection:** Variables named `password`, `secret`, `key`, `token` with string literals.
---
### Unsafe Deserialization
Deserializing untrusted data without validation.
```python
# BAD: Binary serialization from untrusted source can execute arbitrary code
# Examples: Python's binary serialization, yaml.load without SafeLoader
# GOOD: Use safe alternatives
import json
def load_data(file_path):
with open(file_path, 'r') as f:
return json.load(f)
# GOOD: Use SafeLoader for YAML
import yaml
with open('config.yaml') as f:
config = yaml.safe_load(f)
```
**Detection:** Binary deserialization functions, yaml.load without safe loader, dynamic code execution on external data.
---
### Missing Input Validation
Trusting user input without validation.
```typescript
// BAD: No validation
app.post('/user', (req, res) => {
const user = db.create({
name: req.body.name,
email: req.body.email,
role: req.body.role // User can set themselves as admin!
});
res.json(user);
});
// GOOD: Validate and sanitize
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
// role is NOT accepted from input
});
app.post('/user', (req, res) => {
const validated = CreateUserSchema.parse(req.body);
const user = db.create({
...validated,
role: 'user' // Default role, not from input
});
res.json(user);
});
```
**Detection:** Request body/params used directly without validation schema.
---
## Performance Antipatterns
### N+1 Query Problem
Loading related data one record at a time.
```python
# BAD: N+1 queries
def get_orders_with_items():
orders = Order.query.all() # 1 query
for order in orders:
items = OrderItem.query.filter_by(order_id=order.id).all() # N queries
order.items = items
return orders
# GOOD: Eager loading
def get_orders_with_items():
return Order.query.options(
joinedload(Order.items)
).all() # 1 query with JOIN
# GOOD: Batch loading
def get_orders_with_items():
orders = Order.query.all()
order_ids = [o.id for o in orders]
items = OrderItem.query.filter(
OrderItem.order_id.in_(order_ids)
).all() # 2 queries total
# Group items by order_id...
```
**Detection:** Database queries inside loops.
---
### Unbounded Collections
Loading unlimited data into memory.
```go
// BAD: Load all records
func GetAllUsers() ([]User, error) {
return db.Find(&[]User{}) // Could be millions
}
// GOOD: Pagination
func GetUsers(page, pageSize int) ([]User, error) {
offset := (page - 1) * pageSize
return db.Limit(pageSize).Offset(offset).Find(&[]User{})
}
// GOOD: Streaming for large datasets
func ProcessAllUsers(handler func(User) error) error {
rows, err := db.Model(&User{}).Rows()
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var user User
db.ScanRows(rows, &user)
if err := handler(user); err != nil {
return err
}
}
return nil
}
```
**Detection:** `findAll()`, `find({})`, or queries without `LIMIT`.
---
### Synchronous I/O in Hot Paths
Blocking operations in request handlers.
```javascript
// BAD: Sync file read on every request
app.get('/config', (req, res) => {
const config = fs.readFileSync('./config.json'); // Blocks event loop
res.json(JSON.parse(config));
});
// GOOD: Load once at startup
const config = JSON.parse(fs.readFileSync('./config.json'));
app.get('/config', (req, res) => {
res.json(config);
});
// GOOD: Async with caching
let configCache = null;
app.get('/config', async (req, res) => {
if (!configCache) {
configCache = JSON.parse(await fs.promises.readFile('./config.json'));
}
res.json(configCache);
});
```
**Detection:** `readFileSync`, `execSync`, or blocking calls in request handlers.
---
## Testing Antipatterns
### Test Code Duplication
Repeating setup in every test.
```typescript
// BAD: Duplicate setup
describe('UserService', () => {
it('should create user', async () => {
const db = await createTestDatabase();
const userRepo = new UserRepository(db);
const emailService = new MockEmailService();
const service = new UserService(userRepo, emailService);
const user = await service.create({ name: 'Test' });
expect(user.name).toBe('Test');
});
it('should update user', async () => {
const db = await createTestDatabase(); // Duplicated
const userRepo = new UserRepository(db); // Duplicated
const emailService = new MockEmailService(); // Duplicated
const service = new UserService(userRepo, emailService); // Duplicated
// ...
});
});
// GOOD: Shared setup
describe('UserService', () => {
let service: UserService;
let db: TestDatabase;
beforeEach(async () => {
db = await createTestDatabase();
const userRepo = new UserRepository(db);
const emailService = new MockEmailService();
service = new UserService(userRepo, emailService);
});
afterEach(async () => {
await db.cleanup();
});
it('should create user', async () => {
const user = await service.create({ name: 'Test' });
expect(user.name).toBe('Test');
});
});
```
---
### Testing Implementation Instead of Behavior
Tests coupled to internal implementation.
```python
# BAD: Testing implementation details
def test_add_item_to_cart():
cart = ShoppingCart()
cart.add_item(Product("Apple", 1.00))
# Testing internal structure
assert cart._items[0].name == "Apple"
assert cart._total == 1.00
# GOOD: Testing behavior
def test_add_item_to_cart():
cart = ShoppingCart()
cart.add_item(Product("Apple", 1.00))
# Testing public behavior
assert cart.item_count == 1
assert cart.total == 1.00
assert cart.contains("Apple")
```
---
## Async Antipatterns
### Floating Promises
Promises without await or catch.
```typescript
// BAD: Floating promise
async function saveUser(user: User) {
db.save(user); // Not awaited, errors lost
logger.info('User saved'); // Logs before save completes
}
// BAD: Fire and forget in loop
for (const item of items) {
processItem(item); // All run in parallel, no error handling
}
// GOOD: Await the promise
async function saveUser(user: User) {
await db.save(user);
logger.info('User saved');
}
// GOOD: Process with proper handling
await Promise.all(items.map(item => processItem(item)));
// Or sequentially
for (const item of items) {
await processItem(item);
}
```
**Detection:** Async function calls without `await` or `.then()`.
---
### Callback Hell
Deeply nested callbacks.
```javascript
// BAD: Callback hell
getUser(userId, (err, user) => {
if (err) return handleError(err);
getOrders(user.id, (err, orders) => {
if (err) return handleError(err);
getProducts(orders[0].productIds, (err, products) => {
if (err) return handleError(err);
renderPage(user, orders, products, (err) => {
if (err) return handleError(err);
console.log('Done');
});
});
});
});
// GOOD: Async/await
async function loadPage(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const products = await getProducts(orders[0].productIds);
await renderPage(user, orders, products);
console.log('Done');
} catch (err) {
handleError(err);
}
}
```
**Detection:** >2 levels of callback nesting.
---
### Async in Constructor
Async operations in constructors.
```typescript
// BAD: Async in constructor
class DatabaseConnection {
constructor(url: string) {
this.connect(url); // Fire-and-forget async
}
private async connect(url: string) {
this.client = await createClient(url);
}
}
// GOOD: Factory method
class DatabaseConnection {
private constructor(private client: Client) {}
static async create(url: string): Promise<DatabaseConnection> {
const client = await createClient(url);
return new DatabaseConnection(client);
}
}
// Usage
const db = await DatabaseConnection.create(url);
```
**Detection:** `async` calls or `.then()` in constructor.
FILE:scripts/code_quality_checker.py
#!/usr/bin/env python3
"""
Code Quality Checker
Analyzes source code for quality issues, code smells, complexity metrics,
and SOLID principle violations.
Usage:
python code_quality_checker.py /path/to/file.py
python code_quality_checker.py /path/to/directory --recursive
python code_quality_checker.py . --language typescript --json
"""
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional
# Language-specific file extensions
LANGUAGE_EXTENSIONS = {
"python": [".py"],
"typescript": [".ts", ".tsx"],
"javascript": [".js", ".jsx", ".mjs"],
"go": [".go"],
"swift": [".swift"],
"kotlin": [".kt", ".kts"]
}
# Code smell thresholds
THRESHOLDS = {
"long_function_lines": 50,
"too_many_parameters": 5,
"high_complexity": 10,
"god_class_methods": 20,
"max_imports": 15
}
def get_file_extension(filepath: Path) -> str:
"""Get file extension."""
return filepath.suffix.lower()
def detect_language(filepath: Path) -> Optional[str]:
"""Detect programming language from file extension."""
ext = get_file_extension(filepath)
for lang, extensions in LANGUAGE_EXTENSIONS.items():
if ext in extensions:
return lang
return None
def read_file_content(filepath: Path) -> str:
"""Read file content safely."""
try:
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
return f.read()
except Exception:
return ""
def calculate_cyclomatic_complexity(content: str) -> int:
"""
Estimate cyclomatic complexity based on control flow keywords.
"""
complexity = 1 # Base complexity
# Control flow patterns that increase complexity
patterns = [
r"\bif\b",
r"\belif\b",
r"\belse\b",
r"\bfor\b",
r"\bwhile\b",
r"\bcase\b",
r"\bcatch\b",
r"\bexcept\b",
r"\band\b",
r"\bor\b",
r"\|\|",
r"&&"
]
for pattern in patterns:
matches = re.findall(pattern, content, re.IGNORECASE)
complexity += len(matches)
return complexity
def count_lines(content: str) -> Dict[str, int]:
"""Count different types of lines in code."""
lines = content.split("\n")
total = len(lines)
blank = sum(1 for line in lines if not line.strip())
comment = 0
for line in lines:
stripped = line.strip()
if stripped.startswith("#") or stripped.startswith("//"):
comment += 1
elif stripped.startswith("/*") or stripped.startswith("'''") or stripped.startswith('"""'):
comment += 1
code = total - blank - comment
return {
"total": total,
"code": code,
"blank": blank,
"comment": comment
}
def find_functions(content: str, language: str) -> List[Dict]:
"""Find function definitions and their metrics."""
functions = []
# Language-specific function patterns
patterns = {
"python": r"def\s+(\w+)\s*\(([^)]*)\)",
"typescript": r"(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>)",
"javascript": r"(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>)",
"go": r"func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(([^)]*)\)",
"swift": r"func\s+(\w+)\s*\(([^)]*)\)",
"kotlin": r"fun\s+(\w+)\s*\(([^)]*)\)"
}
pattern = patterns.get(language, patterns["python"])
matches = re.finditer(pattern, content, re.MULTILINE)
for match in matches:
name = next((g for g in match.groups() if g), "anonymous")
params_str = match.group(2) if len(match.groups()) > 1 and match.group(2) else ""
# Count parameters
params = [p.strip() for p in params_str.split(",") if p.strip()]
param_count = len(params)
# Estimate function length
start_pos = match.end()
remaining = content[start_pos:]
next_func = re.search(pattern, remaining)
if next_func:
func_body = remaining[:next_func.start()]
else:
func_body = remaining[:min(2000, len(remaining))]
line_count = len(func_body.split("\n"))
complexity = calculate_cyclomatic_complexity(func_body)
functions.append({
"name": name,
"parameters": param_count,
"lines": line_count,
"complexity": complexity
})
return functions
def find_classes(content: str, language: str) -> List[Dict]:
"""Find class definitions and their metrics."""
classes = []
patterns = {
"python": r"class\s+(\w+)",
"typescript": r"class\s+(\w+)",
"javascript": r"class\s+(\w+)",
"go": r"type\s+(\w+)\s+struct",
"swift": r"class\s+(\w+)",
"kotlin": r"class\s+(\w+)"
}
pattern = patterns.get(language, patterns["python"])
matches = re.finditer(pattern, content)
for match in matches:
name = match.group(1)
start_pos = match.end()
remaining = content[start_pos:]
next_class = re.search(pattern, remaining)
if next_class:
class_body = remaining[:next_class.start()]
else:
class_body = remaining
# Count methods
method_patterns = {
"python": r"def\s+\w+\s*\(",
"typescript": r"(?:public|private|protected)?\s*\w+\s*\([^)]*\)\s*[:{]",
"javascript": r"\w+\s*\([^)]*\)\s*\{",
"go": r"func\s+\(",
"swift": r"func\s+\w+",
"kotlin": r"fun\s+\w+"
}
method_pattern = method_patterns.get(language, method_patterns["python"])
methods = len(re.findall(method_pattern, class_body))
classes.append({
"name": name,
"methods": methods,
"lines": len(class_body.split("\n"))
})
return classes
def check_code_smells(content: str, functions: List[Dict], classes: List[Dict]) -> List[Dict]:
"""Check for code smells in the content."""
smells = []
# Long functions
for func in functions:
if func["lines"] > THRESHOLDS["long_function_lines"]:
smells.append({
"type": "long_function",
"severity": "medium",
"message": f"Function '{func['name']}' has {func['lines']} lines (max: {THRESHOLDS['long_function_lines']})",
"location": func["name"]
})
# Too many parameters
for func in functions:
if func["parameters"] > THRESHOLDS["too_many_parameters"]:
smells.append({
"type": "too_many_parameters",
"severity": "low",
"message": f"Function '{func['name']}' has {func['parameters']} parameters (max: {THRESHOLDS['too_many_parameters']})",
"location": func["name"]
})
# High complexity
for func in functions:
if func["complexity"] > THRESHOLDS["high_complexity"]:
severity = "high" if func["complexity"] > 20 else "medium"
smells.append({
"type": "high_complexity",
"severity": severity,
"message": f"Function '{func['name']}' has complexity {func['complexity']} (max: {THRESHOLDS['high_complexity']})",
"location": func["name"]
})
# God classes
for cls in classes:
if cls["methods"] > THRESHOLDS["god_class_methods"]:
smells.append({
"type": "god_class",
"severity": "high",
"message": f"Class '{cls['name']}' has {cls['methods']} methods (max: {THRESHOLDS['god_class_methods']})",
"location": cls["name"]
})
# Magic numbers
magic_pattern = r"\b(?<![.\"\'])\d{3,}\b(?!\.\d)"
for i, line in enumerate(content.split("\n"), 1):
if line.strip().startswith(("#", "//", "import", "from")):
continue
matches = re.findall(magic_pattern, line)
for match in matches[:1]: # One per line
smells.append({
"type": "magic_number",
"severity": "low",
"message": f"Magic number {match} should be a named constant",
"location": f"line {i}"
})
# Commented code patterns
commented_code_pattern = r"^\s*[#//]+\s*(if|for|while|def|function|class|const|let|var)\s"
for i, line in enumerate(content.split("\n"), 1):
if re.match(commented_code_pattern, line, re.IGNORECASE):
smells.append({
"type": "commented_code",
"severity": "low",
"message": "Commented-out code should be removed",
"location": f"line {i}"
})
return smells
def check_solid_violations(content: str) -> List[Dict]:
"""Check for potential SOLID principle violations."""
violations = []
# OCP: Type checking instead of polymorphism
type_checks = len(re.findall(r"isinstance\(|type\(.*\)\s*==|typeof\s+\w+\s*===", content))
if type_checks > 2:
violations.append({
"principle": "OCP",
"name": "Open/Closed Principle",
"severity": "medium",
"message": f"Found {type_checks} type checks - consider using polymorphism"
})
# LSP/ISP: NotImplementedError
not_impl = len(re.findall(r"raise\s+NotImplementedError|not\s+implemented", content, re.IGNORECASE))
if not_impl:
violations.append({
"principle": "LSP/ISP",
"name": "Liskov/Interface Segregation",
"severity": "low",
"message": f"Found {not_impl} unimplemented methods - may indicate oversized interface"
})
# DIP: Too many direct imports
imports = len(re.findall(r"^(?:import|from)\s+", content, re.MULTILINE))
if imports > THRESHOLDS["max_imports"]:
violations.append({
"principle": "DIP",
"name": "Dependency Inversion Principle",
"severity": "low",
"message": f"File has {imports} imports - consider dependency injection"
})
return violations
def calculate_quality_score(
line_metrics: Dict,
functions: List[Dict],
classes: List[Dict],
smells: List[Dict],
violations: List[Dict]
) -> int:
"""Calculate overall quality score (0-100)."""
score = 100
# Deduct for code smells
for smell in smells:
if smell["severity"] == "high":
score -= 10
elif smell["severity"] == "medium":
score -= 5
elif smell["severity"] == "low":
score -= 2
# Deduct for SOLID violations
for violation in violations:
if violation["severity"] == "high":
score -= 8
elif violation["severity"] == "medium":
score -= 4
elif violation["severity"] == "low":
score -= 2
# Bonus for good comment ratio (10-30%)
if line_metrics["total"] > 0:
comment_ratio = line_metrics["comment"] / line_metrics["total"]
if 0.1 <= comment_ratio <= 0.3:
score += 5
# Bonus for reasonable function sizes
if functions:
avg_lines = sum(f["lines"] for f in functions) / len(functions)
if avg_lines < 30:
score += 5
return max(0, min(100, score))
def get_grade(score: int) -> str:
"""Convert score to letter grade."""
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F"
def analyze_file(filepath: Path) -> Dict:
"""Analyze a single file for code quality."""
language = detect_language(filepath)
if not language:
return {"error": f"Unsupported file type: {filepath.suffix}"}
content = read_file_content(filepath)
if not content:
return {"error": f"Could not read file: {filepath}"}
line_metrics = count_lines(content)
functions = find_functions(content, language)
classes = find_classes(content, language)
smells = check_code_smells(content, functions, classes)
violations = check_solid_violations(content)
score = calculate_quality_score(line_metrics, functions, classes, smells, violations)
return {
"file": str(filepath),
"language": language,
"metrics": {
"lines": line_metrics,
"functions": len(functions),
"classes": len(classes),
"avg_complexity": round(sum(f["complexity"] for f in functions) / max(1, len(functions)), 1)
},
"quality_score": score,
"grade": get_grade(score),
"smells": smells,
"solid_violations": violations,
"function_details": functions[:10],
"class_details": classes[:10]
}
def analyze_directory(
dir_path: Path,
recursive: bool = True,
language: Optional[str] = None
) -> Dict:
"""Analyze all files in a directory."""
results = []
extensions = []
if language:
extensions = LANGUAGE_EXTENSIONS.get(language, [])
else:
for exts in LANGUAGE_EXTENSIONS.values():
extensions.extend(exts)
pattern = "**/*" if recursive else "*"
for ext in extensions:
for filepath in dir_path.glob(f"{pattern}{ext}"):
if "node_modules" in str(filepath) or ".git" in str(filepath):
continue
result = analyze_file(filepath)
if "error" not in result:
results.append(result)
if not results:
return {"error": "No supported files found"}
total_score = sum(r["quality_score"] for r in results)
avg_score = total_score / len(results)
total_smells = sum(len(r["smells"]) for r in results)
total_violations = sum(len(r["solid_violations"]) for r in results)
return {
"directory": str(dir_path),
"files_analyzed": len(results),
"average_score": round(avg_score, 1),
"overall_grade": get_grade(int(avg_score)),
"total_code_smells": total_smells,
"total_solid_violations": total_violations,
"files": sorted(results, key=lambda x: x["quality_score"])
}
def print_report(analysis: Dict) -> None:
"""Print human-readable analysis report."""
if "error" in analysis:
print(f"Error: {analysis['error']}")
return
print("=" * 60)
print("CODE QUALITY REPORT")
print("=" * 60)
if "file" in analysis:
print(f"\nFile: {analysis['file']}")
print(f"Language: {analysis['language']}")
print(f"Quality Score: {analysis['quality_score']}/100 ({analysis['grade']})")
metrics = analysis["metrics"]
print(f"\nLines: {metrics['lines']['total']} ({metrics['lines']['code']} code, {metrics['lines']['comment']} comments)")
print(f"Functions: {metrics['functions']}")
print(f"Classes: {metrics['classes']}")
print(f"Avg Complexity: {metrics['avg_complexity']}")
if analysis["smells"]:
print("\n--- CODE SMELLS ---")
for smell in analysis["smells"][:10]:
print(f" [{smell['severity'].upper()}] {smell['message']} ({smell['location']})")
if analysis["solid_violations"]:
print("\n--- SOLID VIOLATIONS ---")
for v in analysis["solid_violations"]:
print(f" [{v['principle']}] {v['message']}")
else:
print(f"\nDirectory: {analysis['directory']}")
print(f"Files Analyzed: {analysis['files_analyzed']}")
print(f"Average Score: {analysis['average_score']}/100 ({analysis['overall_grade']})")
print(f"Total Code Smells: {analysis['total_code_smells']}")
print(f"Total SOLID Violations: {analysis['total_solid_violations']}")
print("\n--- FILES BY QUALITY ---")
for f in analysis["files"][:10]:
print(f" {f['quality_score']:3d}/100 [{f['grade']}] {f['file']}")
print("\n" + "=" * 60)
def main():
parser = argparse.ArgumentParser(
description="Analyze code quality, smells, and SOLID violations"
)
parser.add_argument(
"path",
help="File or directory to analyze"
)
parser.add_argument(
"--recursive", "-r",
action="store_true",
default=True,
help="Recursively analyze directories (default: true)"
)
parser.add_argument(
"--language", "-l",
choices=list(LANGUAGE_EXTENSIONS.keys()),
help="Filter by programming language"
)
parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format"
)
parser.add_argument(
"--output", "-o",
help="Write output to file"
)
args = parser.parse_args()
target = Path(args.path).resolve()
if not target.exists():
print(f"Error: Path does not exist: {target}", file=sys.stderr)
sys.exit(1)
if target.is_file():
analysis = analyze_file(target)
else:
analysis = analyze_directory(target, args.recursive, args.language)
if args.json:
output = json.dumps(analysis, indent=2, default=str)
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"Results written to {args.output}")
else:
print(output)
else:
print_report(analysis)
if __name__ == "__main__":
main()
FILE:scripts/pr_analyzer.py
#!/usr/bin/env python3
"""
PR Analyzer
Analyzes pull request changes for review complexity, risk assessment,
and generates review priorities.
Usage:
python pr_analyzer.py /path/to/repo
python pr_analyzer.py . --base main --head feature-branch
python pr_analyzer.py /path/to/repo --json
"""
import argparse
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple
# File categories for review prioritization
FILE_CATEGORIES = {
"critical": {
"patterns": [
r"auth", r"security", r"password", r"token", r"secret",
r"payment", r"billing", r"crypto", r"encrypt"
],
"weight": 5,
"description": "Security-sensitive files requiring careful review"
},
"high": {
"patterns": [
r"api", r"database", r"migration", r"schema", r"model",
r"config", r"env", r"middleware"
],
"weight": 4,
"description": "Core infrastructure files"
},
"medium": {
"patterns": [
r"service", r"controller", r"handler", r"util", r"helper"
],
"weight": 3,
"description": "Business logic files"
},
"low": {
"patterns": [
r"test", r"spec", r"mock", r"fixture", r"story",
r"readme", r"docs", r"\.md$"
],
"weight": 1,
"description": "Tests and documentation"
}
}
# Risky patterns to flag
RISK_PATTERNS = [
{
"name": "hardcoded_secrets",
"pattern": r"(password|secret|api_key|token)\s*[=:]\s*['\"][^'\"]+['\"]",
"severity": "critical",
"message": "Potential hardcoded secret detected"
},
{
"name": "todo_fixme",
"pattern": r"(TODO|FIXME|HACK|XXX):",
"severity": "low",
"message": "TODO/FIXME comment found"
},
{
"name": "console_log",
"pattern": r"console\.(log|debug|info|warn|error)\(",
"severity": "medium",
"message": "Console statement found (remove for production)"
},
{
"name": "debugger",
"pattern": r"\bdebugger\b",
"severity": "high",
"message": "Debugger statement found"
},
{
"name": "disable_eslint",
"pattern": r"eslint-disable",
"severity": "medium",
"message": "ESLint rule disabled"
},
{
"name": "any_type",
"pattern": r":\s*any\b",
"severity": "medium",
"message": "TypeScript 'any' type used"
},
{
"name": "sql_concatenation",
"pattern": r"(SELECT|INSERT|UPDATE|DELETE).*\+.*['\"]",
"severity": "critical",
"message": "Potential SQL injection (string concatenation in query)"
}
]
def run_git_command(cmd: List[str], cwd: Path) -> Tuple[bool, str]:
"""Run a git command and return success status and output."""
try:
result = subprocess.run(
cmd,
cwd=cwd,
capture_output=True,
text=True,
timeout=30
)
return result.returncode == 0, result.stdout.strip()
except subprocess.TimeoutExpired:
return False, "Command timed out"
except Exception as e:
return False, str(e)
def get_changed_files(repo_path: Path, base: str, head: str) -> List[Dict]:
"""Get list of changed files between two refs."""
success, output = run_git_command(
["git", "diff", "--name-status", f"{base}...{head}"],
repo_path
)
if not success:
# Try without the triple dot (for uncommitted changes)
success, output = run_git_command(
["git", "diff", "--name-status", base, head],
repo_path
)
if not success or not output:
# Fall back to staged changes
success, output = run_git_command(
["git", "diff", "--name-status", "--cached"],
repo_path
)
files = []
for line in output.split("\n"):
if not line.strip():
continue
parts = line.split("\t")
if len(parts) >= 2:
status = parts[0][0] # First character of status
filepath = parts[-1] # Handle renames (R100\told\tnew)
status_map = {
"A": "added",
"M": "modified",
"D": "deleted",
"R": "renamed",
"C": "copied"
}
files.append({
"path": filepath,
"status": status_map.get(status, "modified")
})
return files
def get_file_diff(repo_path: Path, filepath: str, base: str, head: str) -> str:
"""Get diff content for a specific file."""
success, output = run_git_command(
["git", "diff", f"{base}...{head}", "--", filepath],
repo_path
)
if not success:
success, output = run_git_command(
["git", "diff", "--cached", "--", filepath],
repo_path
)
return output if success else ""
def categorize_file(filepath: str) -> Tuple[str, int]:
"""Categorize a file based on its path and name."""
filepath_lower = filepath.lower()
for category, info in FILE_CATEGORIES.items():
for pattern in info["patterns"]:
if re.search(pattern, filepath_lower):
return category, info["weight"]
return "medium", 2 # Default category
def analyze_diff_for_risks(diff_content: str, filepath: str) -> List[Dict]:
"""Analyze diff content for risky patterns."""
risks = []
# Only analyze added lines (starting with +)
added_lines = [
line[1:] for line in diff_content.split("\n")
if line.startswith("+") and not line.startswith("+++")
]
content = "\n".join(added_lines)
for risk in RISK_PATTERNS:
matches = re.findall(risk["pattern"], content, re.IGNORECASE)
if matches:
risks.append({
"name": risk["name"],
"severity": risk["severity"],
"message": risk["message"],
"file": filepath,
"count": len(matches)
})
return risks
def count_changes(diff_content: str) -> Dict[str, int]:
"""Count additions and deletions in diff."""
additions = 0
deletions = 0
for line in diff_content.split("\n"):
if line.startswith("+") and not line.startswith("+++"):
additions += 1
elif line.startswith("-") and not line.startswith("---"):
deletions += 1
return {"additions": additions, "deletions": deletions}
def calculate_complexity_score(files: List[Dict], all_risks: List[Dict]) -> int:
"""Calculate overall PR complexity score (1-10)."""
score = 0
# File count contribution (max 3 points)
file_count = len(files)
if file_count > 20:
score += 3
elif file_count > 10:
score += 2
elif file_count > 5:
score += 1
# Total changes contribution (max 3 points)
total_changes = sum(f.get("additions", 0) + f.get("deletions", 0) for f in files)
if total_changes > 500:
score += 3
elif total_changes > 200:
score += 2
elif total_changes > 50:
score += 1
# Risk severity contribution (max 4 points)
critical_risks = sum(1 for r in all_risks if r["severity"] == "critical")
high_risks = sum(1 for r in all_risks if r["severity"] == "high")
score += min(2, critical_risks)
score += min(2, high_risks)
return min(10, max(1, score))
def analyze_commit_messages(repo_path: Path, base: str, head: str) -> Dict:
"""Analyze commit messages in the PR."""
success, output = run_git_command(
["git", "log", "--oneline", f"{base}...{head}"],
repo_path
)
if not success or not output:
return {"commits": 0, "issues": []}
commits = output.strip().split("\n")
issues = []
for commit in commits:
if len(commit) < 10:
continue
# Check for conventional commit format
message = commit[8:] if len(commit) > 8 else commit # Skip hash
if not re.match(r"^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?:", message):
issues.append({
"commit": commit[:7],
"issue": "Does not follow conventional commit format"
})
if len(message) > 72:
issues.append({
"commit": commit[:7],
"issue": "Commit message exceeds 72 characters"
})
return {
"commits": len(commits),
"issues": issues
}
def analyze_pr(
repo_path: Path,
base: str = "main",
head: str = "HEAD"
) -> Dict:
"""Perform complete PR analysis."""
# Get changed files
changed_files = get_changed_files(repo_path, base, head)
if not changed_files:
return {
"status": "no_changes",
"message": "No changes detected between branches"
}
# Analyze each file
all_risks = []
file_analyses = []
for file_info in changed_files:
filepath = file_info["path"]
category, weight = categorize_file(filepath)
# Get diff for the file
diff = get_file_diff(repo_path, filepath, base, head)
changes = count_changes(diff)
risks = analyze_diff_for_risks(diff, filepath)
all_risks.extend(risks)
file_analyses.append({
"path": filepath,
"status": file_info["status"],
"category": category,
"priority_weight": weight,
"additions": changes["additions"],
"deletions": changes["deletions"],
"risks": risks
})
# Sort by priority (highest first)
file_analyses.sort(key=lambda x: (-x["priority_weight"], x["path"]))
# Analyze commits
commit_analysis = analyze_commit_messages(repo_path, base, head)
# Calculate metrics
complexity = calculate_complexity_score(file_analyses, all_risks)
total_additions = sum(f["additions"] for f in file_analyses)
total_deletions = sum(f["deletions"] for f in file_analyses)
return {
"status": "analyzed",
"summary": {
"files_changed": len(file_analyses),
"total_additions": total_additions,
"total_deletions": total_deletions,
"complexity_score": complexity,
"complexity_label": get_complexity_label(complexity),
"commits": commit_analysis["commits"]
},
"risks": {
"critical": [r for r in all_risks if r["severity"] == "critical"],
"high": [r for r in all_risks if r["severity"] == "high"],
"medium": [r for r in all_risks if r["severity"] == "medium"],
"low": [r for r in all_risks if r["severity"] == "low"]
},
"files": file_analyses,
"commit_issues": commit_analysis["issues"],
"review_order": [f["path"] for f in file_analyses[:10]] # Top 10 priority files
}
def get_complexity_label(score: int) -> str:
"""Get human-readable complexity label."""
if score <= 2:
return "Simple"
elif score <= 4:
return "Moderate"
elif score <= 6:
return "Complex"
elif score <= 8:
return "Very Complex"
else:
return "Critical"
def print_report(analysis: Dict) -> None:
"""Print human-readable analysis report."""
if analysis["status"] == "no_changes":
print("No changes detected.")
return
summary = analysis["summary"]
risks = analysis["risks"]
print("=" * 60)
print("PR ANALYSIS REPORT")
print("=" * 60)
print(f"\nComplexity: {summary['complexity_score']}/10 ({summary['complexity_label']})")
print(f"Files Changed: {summary['files_changed']}")
print(f"Lines: +{summary['total_additions']} / -{summary['total_deletions']}")
print(f"Commits: {summary['commits']}")
# Risk summary
print("\n--- RISK SUMMARY ---")
print(f"Critical: {len(risks['critical'])}")
print(f"High: {len(risks['high'])}")
print(f"Medium: {len(risks['medium'])}")
print(f"Low: {len(risks['low'])}")
# Critical and high risks details
if risks["critical"]:
print("\n--- CRITICAL RISKS ---")
for risk in risks["critical"]:
print(f" [{risk['file']}] {risk['message']} (x{risk['count']})")
if risks["high"]:
print("\n--- HIGH RISKS ---")
for risk in risks["high"]:
print(f" [{risk['file']}] {risk['message']} (x{risk['count']})")
# Commit message issues
if analysis["commit_issues"]:
print("\n--- COMMIT MESSAGE ISSUES ---")
for issue in analysis["commit_issues"][:5]:
print(f" {issue['commit']}: {issue['issue']}")
# Review order
print("\n--- SUGGESTED REVIEW ORDER ---")
for i, filepath in enumerate(analysis["review_order"], 1):
file_info = next(f for f in analysis["files"] if f["path"] == filepath)
print(f" {i}. [{file_info['category'].upper()}] {filepath}")
print("\n" + "=" * 60)
def main():
parser = argparse.ArgumentParser(
description="Analyze pull request for review complexity and risks"
)
parser.add_argument(
"repo_path",
nargs="?",
default=".",
help="Path to git repository (default: current directory)"
)
parser.add_argument(
"--base", "-b",
default="main",
help="Base branch for comparison (default: main)"
)
parser.add_argument(
"--head", "-h",
default="HEAD",
help="Head branch/commit for comparison (default: HEAD)"
)
parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format"
)
parser.add_argument(
"--output", "-o",
help="Write output to file"
)
args = parser.parse_args()
repo_path = Path(args.repo_path).resolve()
if not (repo_path / ".git").exists():
print(f"Error: {repo_path} is not a git repository", file=sys.stderr)
sys.exit(1)
analysis = analyze_pr(repo_path, args.base, args.head)
if args.json:
output = json.dumps(analysis, indent=2)
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"Results written to {args.output}")
else:
print(output)
else:
print_report(analysis)
if __name__ == "__main__":
main()
FILE:scripts/review_report_generator.py
#!/usr/bin/env python3
"""
Review Report Generator
Generates comprehensive code review reports by combining PR analysis
and code quality findings into structured, actionable reports.
Usage:
python review_report_generator.py /path/to/repo
python review_report_generator.py . --pr-analysis pr_results.json --quality-analysis quality_results.json
python review_report_generator.py /path/to/repo --format markdown --output review.md
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
# Severity weights for prioritization
SEVERITY_WEIGHTS = {
"critical": 100,
"high": 75,
"medium": 50,
"low": 25,
"info": 10
}
# Review verdict thresholds
VERDICT_THRESHOLDS = {
"approve": {"max_critical": 0, "max_high": 0, "max_score": 100},
"approve_with_suggestions": {"max_critical": 0, "max_high": 2, "max_score": 85},
"request_changes": {"max_critical": 0, "max_high": 5, "max_score": 70},
"block": {"max_critical": float("inf"), "max_high": float("inf"), "max_score": 0}
}
def load_json_file(filepath: str) -> Optional[Dict]:
"""Load JSON file if it exists."""
try:
with open(filepath, "r") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return None
def run_pr_analyzer(repo_path: Path) -> Dict:
"""Run pr_analyzer.py and return results."""
script_path = Path(__file__).parent / "pr_analyzer.py"
if not script_path.exists():
return {"status": "error", "message": "pr_analyzer.py not found"}
try:
result = subprocess.run(
[sys.executable, str(script_path), str(repo_path), "--json"],
capture_output=True,
text=True,
timeout=120
)
if result.returncode == 0:
return json.loads(result.stdout)
return {"status": "error", "message": result.stderr}
except Exception as e:
return {"status": "error", "message": str(e)}
def run_quality_checker(repo_path: Path) -> Dict:
"""Run code_quality_checker.py and return results."""
script_path = Path(__file__).parent / "code_quality_checker.py"
if not script_path.exists():
return {"status": "error", "message": "code_quality_checker.py not found"}
try:
result = subprocess.run(
[sys.executable, str(script_path), str(repo_path), "--json"],
capture_output=True,
text=True,
timeout=300
)
if result.returncode == 0:
return json.loads(result.stdout)
return {"status": "error", "message": result.stderr}
except Exception as e:
return {"status": "error", "message": str(e)}
def calculate_review_score(pr_analysis: Dict, quality_analysis: Dict) -> int:
"""Calculate overall review score (0-100)."""
score = 100
# Deduct for PR risks
if "risks" in pr_analysis:
risks = pr_analysis["risks"]
score -= len(risks.get("critical", [])) * 15
score -= len(risks.get("high", [])) * 10
score -= len(risks.get("medium", [])) * 5
score -= len(risks.get("low", [])) * 2
# Deduct for code quality issues
if "issues" in quality_analysis:
issues = quality_analysis["issues"]
score -= len([i for i in issues if i.get("severity") == "critical"]) * 12
score -= len([i for i in issues if i.get("severity") == "high"]) * 8
score -= len([i for i in issues if i.get("severity") == "medium"]) * 4
score -= len([i for i in issues if i.get("severity") == "low"]) * 1
# Deduct for complexity
if "summary" in pr_analysis:
complexity = pr_analysis["summary"].get("complexity_score", 0)
if complexity > 7:
score -= 10
elif complexity > 5:
score -= 5
return max(0, min(100, score))
def determine_verdict(score: int, critical_count: int, high_count: int) -> Tuple[str, str]:
"""Determine review verdict based on score and issue counts."""
if critical_count > 0:
return "block", "Critical issues must be resolved before merge"
if score >= 90 and high_count == 0:
return "approve", "Code meets quality standards"
if score >= 75 and high_count <= 2:
return "approve_with_suggestions", "Minor improvements recommended"
if score >= 50:
return "request_changes", "Several issues need to be addressed"
return "block", "Significant issues prevent approval"
def generate_findings_list(pr_analysis: Dict, quality_analysis: Dict) -> List[Dict]:
"""Combine and prioritize all findings."""
findings = []
# Add PR risk findings
if "risks" in pr_analysis:
for severity, items in pr_analysis["risks"].items():
for item in items:
findings.append({
"source": "pr_analysis",
"severity": severity,
"category": item.get("name", "unknown"),
"message": item.get("message", ""),
"file": item.get("file", ""),
"count": item.get("count", 1)
})
# Add code quality findings
if "issues" in quality_analysis:
for issue in quality_analysis["issues"]:
findings.append({
"source": "quality_analysis",
"severity": issue.get("severity", "medium"),
"category": issue.get("type", "unknown"),
"message": issue.get("message", ""),
"file": issue.get("file", ""),
"line": issue.get("line", 0)
})
# Sort by severity weight
findings.sort(
key=lambda x: -SEVERITY_WEIGHTS.get(x["severity"], 0)
)
return findings
def generate_action_items(findings: List[Dict]) -> List[Dict]:
"""Generate prioritized action items from findings."""
action_items = []
seen_categories = set()
for finding in findings:
category = finding["category"]
severity = finding["severity"]
# Group similar issues
if category in seen_categories and severity not in ["critical", "high"]:
continue
action = {
"priority": "P0" if severity == "critical" else "P1" if severity == "high" else "P2",
"action": get_action_for_category(category, finding),
"severity": severity,
"files_affected": [finding["file"]] if finding.get("file") else []
}
action_items.append(action)
seen_categories.add(category)
return action_items[:15] # Top 15 actions
def get_action_for_category(category: str, finding: Dict) -> str:
"""Get actionable recommendation for issue category."""
actions = {
"hardcoded_secrets": "Remove hardcoded credentials and use environment variables or a secrets manager",
"sql_concatenation": "Use parameterized queries to prevent SQL injection",
"debugger": "Remove debugger statements before merging",
"console_log": "Remove or replace console statements with proper logging",
"todo_fixme": "Address TODO/FIXME comments or create tracking issues",
"disable_eslint": "Address the underlying issue instead of disabling lint rules",
"any_type": "Replace 'any' types with proper type definitions",
"long_function": "Break down function into smaller, focused units",
"god_class": "Split class into smaller, single-responsibility classes",
"too_many_params": "Use parameter objects or builder pattern",
"deep_nesting": "Refactor using early returns, guard clauses, or extraction",
"high_complexity": "Reduce cyclomatic complexity through refactoring",
"missing_error_handling": "Add proper error handling and recovery logic",
"duplicate_code": "Extract duplicate code into shared functions",
"magic_numbers": "Replace magic numbers with named constants",
"large_file": "Consider splitting into multiple smaller modules"
}
return actions.get(category, f"Review and address: {finding.get('message', category)}")
def format_markdown_report(report: Dict) -> str:
"""Generate markdown-formatted report."""
lines = []
# Header
lines.append("# Code Review Report")
lines.append("")
lines.append(f"**Generated:** {report['metadata']['generated_at']}")
lines.append(f"**Repository:** {report['metadata']['repository']}")
lines.append("")
# Executive Summary
lines.append("## Executive Summary")
lines.append("")
summary = report["summary"]
verdict = summary["verdict"]
verdict_emoji = {
"approve": "✅",
"approve_with_suggestions": "✅",
"request_changes": "⚠️",
"block": "❌"
}.get(verdict, "❓")
lines.append(f"**Verdict:** {verdict_emoji} {verdict.upper().replace('_', ' ')}")
lines.append(f"**Score:** {summary['score']}/100")
lines.append(f"**Rationale:** {summary['rationale']}")
lines.append("")
# Issue Counts
lines.append("### Issue Summary")
lines.append("")
lines.append("| Severity | Count |")
lines.append("|----------|-------|")
for severity in ["critical", "high", "medium", "low"]:
count = summary["issue_counts"].get(severity, 0)
lines.append(f"| {severity.capitalize()} | {count} |")
lines.append("")
# PR Statistics (if available)
if "pr_summary" in report:
pr = report["pr_summary"]
lines.append("### Change Statistics")
lines.append("")
lines.append(f"- **Files Changed:** {pr.get('files_changed', 'N/A')}")
lines.append(f"- **Lines Added:** +{pr.get('total_additions', 0)}")
lines.append(f"- **Lines Removed:** -{pr.get('total_deletions', 0)}")
lines.append(f"- **Complexity:** {pr.get('complexity_label', 'N/A')}")
lines.append("")
# Action Items
if report.get("action_items"):
lines.append("## Action Items")
lines.append("")
for i, item in enumerate(report["action_items"], 1):
priority = item["priority"]
emoji = "🔴" if priority == "P0" else "🟠" if priority == "P1" else "🟡"
lines.append(f"{i}. {emoji} **[{priority}]** {item['action']}")
if item.get("files_affected"):
lines.append(f" - Files: {', '.join(item['files_affected'][:3])}")
lines.append("")
# Critical Findings
critical_findings = [f for f in report.get("findings", []) if f["severity"] == "critical"]
if critical_findings:
lines.append("## Critical Issues (Must Fix)")
lines.append("")
for finding in critical_findings:
lines.append(f"- **{finding['category']}** in `{finding.get('file', 'unknown')}`")
lines.append(f" - {finding['message']}")
lines.append("")
# High Priority Findings
high_findings = [f for f in report.get("findings", []) if f["severity"] == "high"]
if high_findings:
lines.append("## High Priority Issues")
lines.append("")
for finding in high_findings[:10]:
lines.append(f"- **{finding['category']}** in `{finding.get('file', 'unknown')}`")
lines.append(f" - {finding['message']}")
lines.append("")
# Review Order (if available)
if "review_order" in report:
lines.append("## Suggested Review Order")
lines.append("")
for i, filepath in enumerate(report["review_order"][:10], 1):
lines.append(f"{i}. `{filepath}`")
lines.append("")
# Footer
lines.append("---")
lines.append("*Generated by Code Reviewer*")
return "\n".join(lines)
def format_text_report(report: Dict) -> str:
"""Generate plain text report."""
lines = []
lines.append("=" * 60)
lines.append("CODE REVIEW REPORT")
lines.append("=" * 60)
lines.append("")
lines.append(f"Generated: {report['metadata']['generated_at']}")
lines.append(f"Repository: {report['metadata']['repository']}")
lines.append("")
summary = report["summary"]
verdict = summary["verdict"].upper().replace("_", " ")
lines.append(f"VERDICT: {verdict}")
lines.append(f"SCORE: {summary['score']}/100")
lines.append(f"RATIONALE: {summary['rationale']}")
lines.append("")
lines.append("--- ISSUE SUMMARY ---")
for severity in ["critical", "high", "medium", "low"]:
count = summary["issue_counts"].get(severity, 0)
lines.append(f" {severity.capitalize()}: {count}")
lines.append("")
if report.get("action_items"):
lines.append("--- ACTION ITEMS ---")
for i, item in enumerate(report["action_items"][:10], 1):
lines.append(f" {i}. [{item['priority']}] {item['action']}")
lines.append("")
critical = [f for f in report.get("findings", []) if f["severity"] == "critical"]
if critical:
lines.append("--- CRITICAL ISSUES ---")
for f in critical:
lines.append(f" [{f.get('file', 'unknown')}] {f['message']}")
lines.append("")
lines.append("=" * 60)
return "\n".join(lines)
def generate_report(
repo_path: Path,
pr_analysis: Optional[Dict] = None,
quality_analysis: Optional[Dict] = None
) -> Dict:
"""Generate comprehensive review report."""
# Run analyses if not provided
if pr_analysis is None:
pr_analysis = run_pr_analyzer(repo_path)
if quality_analysis is None:
quality_analysis = run_quality_checker(repo_path)
# Generate findings
findings = generate_findings_list(pr_analysis, quality_analysis)
# Count issues by severity
issue_counts = {
"critical": len([f for f in findings if f["severity"] == "critical"]),
"high": len([f for f in findings if f["severity"] == "high"]),
"medium": len([f for f in findings if f["severity"] == "medium"]),
"low": len([f for f in findings if f["severity"] == "low"])
}
# Calculate score and verdict
score = calculate_review_score(pr_analysis, quality_analysis)
verdict, rationale = determine_verdict(
score,
issue_counts["critical"],
issue_counts["high"]
)
# Generate action items
action_items = generate_action_items(findings)
# Build report
report = {
"metadata": {
"generated_at": datetime.now().isoformat(),
"repository": str(repo_path),
"version": "1.0.0"
},
"summary": {
"score": score,
"verdict": verdict,
"rationale": rationale,
"issue_counts": issue_counts
},
"findings": findings,
"action_items": action_items
}
# Add PR summary if available
if pr_analysis.get("status") == "analyzed":
report["pr_summary"] = pr_analysis.get("summary", {})
report["review_order"] = pr_analysis.get("review_order", [])
# Add quality summary if available
if quality_analysis.get("status") == "analyzed":
report["quality_summary"] = quality_analysis.get("summary", {})
return report
def main():
parser = argparse.ArgumentParser(
description="Generate comprehensive code review reports"
)
parser.add_argument(
"repo_path",
nargs="?",
default=".",
help="Path to repository (default: current directory)"
)
parser.add_argument(
"--pr-analysis",
help="Path to pre-computed PR analysis JSON"
)
parser.add_argument(
"--quality-analysis",
help="Path to pre-computed quality analysis JSON"
)
parser.add_argument(
"--format", "-f",
choices=["text", "markdown", "json"],
default="text",
help="Output format (default: text)"
)
parser.add_argument(
"--output", "-o",
help="Write output to file"
)
parser.add_argument(
"--json",
action="store_true",
help="Output as JSON (shortcut for --format json)"
)
args = parser.parse_args()
repo_path = Path(args.repo_path).resolve()
if not repo_path.exists():
print(f"Error: Path does not exist: {repo_path}", file=sys.stderr)
sys.exit(1)
# Load pre-computed analyses if provided
pr_analysis = None
quality_analysis = None
if args.pr_analysis:
pr_analysis = load_json_file(args.pr_analysis)
if not pr_analysis:
print(f"Warning: Could not load PR analysis from {args.pr_analysis}")
if args.quality_analysis:
quality_analysis = load_json_file(args.quality_analysis)
if not quality_analysis:
print(f"Warning: Could not load quality analysis from {args.quality_analysis}")
# Generate report
report = generate_report(repo_path, pr_analysis, quality_analysis)
# Format output
output_format = "json" if args.json else args.format
if output_format == "json":
output = json.dumps(report, indent=2)
elif output_format == "markdown":
output = format_markdown_report(report)
else:
output = format_text_report(report)
# Write or print output
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"Report written to {args.output}")
else:
print(output)
if __name__ == "__main__":
main()
Analyzes RFP/RFI responses for coverage gaps, builds competitive feature comparison matrices, and plans proof-of-concept (POC) engagements for pre-sales engi...
---
name: "sales-engineer"
description: Analyzes RFP/RFI responses for coverage gaps, builds competitive feature comparison matrices, and plans proof-of-concept (POC) engagements for pre-sales engineering. Use when responding to RFPs, bids, or proposal requests; comparing product features against competitors; planning or scoring a customer POC or sales demo; preparing a technical proposal; or performing win/loss competitor analysis. Handles tasks described as 'RFP response', 'bid response', 'proposal response', 'competitor comparison', 'feature matrix', 'POC planning', 'sales demo prep', or 'pre-sales engineering'.
---
# Sales Engineer Skill
## 5-Phase Workflow
### Phase 1: Discovery & Research
**Objective:** Understand customer requirements, technical environment, and business drivers.
**Checklist:**
- [ ] Conduct technical discovery calls with stakeholders
- [ ] Map customer's current architecture and pain points
- [ ] Identify integration requirements and constraints
- [ ] Document security and compliance requirements
- [ ] Assess competitive landscape for this opportunity
**Tools:** Run `rfp_response_analyzer.py` to score initial requirement alignment.
```bash
python scripts/rfp_response_analyzer.py assets/sample_rfp_data.json --format json > phase1_rfp_results.json
```
**Output:** Technical discovery document, requirement map, initial coverage assessment.
**Validation checkpoint:** Coverage score must be >50% and must-have gaps ≤3 before proceeding to Phase 2. Check with:
```bash
python scripts/rfp_response_analyzer.py assets/sample_rfp_data.json --format json | python -c "import sys,json; r=json.load(sys.stdin); print('PROCEED' if r['coverage_score']>50 and r['must_have_gaps']<=3 else 'REVIEW')"
```
---
### Phase 2: Solution Design
**Objective:** Design a solution architecture that addresses customer requirements.
**Checklist:**
- [ ] Map product capabilities to customer requirements
- [ ] Design integration architecture
- [ ] Identify customization needs and development effort
- [ ] Build competitive differentiation strategy
- [ ] Create solution architecture diagrams
**Tools:** Run `competitive_matrix_builder.py` using Phase 1 data to identify differentiators and vulnerabilities.
```bash
python scripts/competitive_matrix_builder.py competitive_data.json --format json > phase2_competitive.json
python -c "import json; d=json.load(open('phase2_competitive.json')); print('Differentiators:', d['differentiators']); print('Vulnerabilities:', d['vulnerabilities'])"
```
**Output:** Solution architecture, competitive positioning, technical differentiation strategy.
**Validation checkpoint:** Confirm at least one strong differentiator exists per customer priority before proceeding to Phase 3. If no differentiators found, escalate to Product Team (see Integration Points).
---
### Phase 3: Demo Preparation & Delivery
**Objective:** Deliver compelling technical demonstrations tailored to stakeholder priorities.
**Checklist:**
- [ ] Build demo environment matching customer's use case
- [ ] Create demo script with talking points per stakeholder role
- [ ] Prepare objection handling responses
- [ ] Rehearse failure scenarios and recovery paths
- [ ] Collect feedback and adjust approach
**Templates:** Use `assets/demo_script_template.md` for structured demo preparation.
**Output:** Customized demo, stakeholder-specific talking points, feedback capture.
**Validation checkpoint:** Demo script must cover every must-have requirement flagged in `phase1_rfp_results.json` before delivery. Cross-reference with:
```bash
python -c "import json; rfp=json.load(open('phase1_rfp_results.json')); [print('UNCOVERED:', r) for r in rfp['must_have_requirements'] if r['coverage']=='Gap']"
```
---
### Phase 4: POC & Evaluation
**Objective:** Execute a structured proof-of-concept that validates the solution.
**Checklist:**
- [ ] Define POC scope, success criteria, and timeline
- [ ] Allocate resources and set up environment
- [ ] Execute phased testing (core, advanced, edge cases)
- [ ] Track progress against success criteria
- [ ] Generate evaluation scorecard
**Tools:** Run `poc_planner.py` to generate the complete POC plan.
```bash
python scripts/poc_planner.py poc_data.json --format json > phase4_poc_plan.json
python -c "import json; p=json.load(open('phase4_poc_plan.json')); print('Go/No-Go:', p['recommendation'])"
```
**Templates:** Use `assets/poc_scorecard_template.md` for evaluation tracking.
**Output:** POC plan, evaluation scorecard, go/no-go recommendation.
**Validation checkpoint:** POC conversion requires scorecard score >60% across all evaluation dimensions (functionality, performance, integration, usability, support). If score <60%, document gaps and loop back to Phase 2 for solution redesign.
---
### Phase 5: Proposal & Closing
**Objective:** Deliver a technical proposal that supports the commercial close.
**Checklist:**
- [ ] Compile POC results and success metrics
- [ ] Create technical proposal with implementation plan
- [ ] Address outstanding objections with evidence
- [ ] Support pricing and packaging discussions
- [ ] Conduct win/loss analysis post-decision
**Templates:** Use `assets/technical_proposal_template.md` for the proposal document.
**Output:** Technical proposal, implementation timeline, risk mitigation plan.
---
## Python Automation Tools
### 1. RFP Response Analyzer
**Script:** `scripts/rfp_response_analyzer.py`
**Purpose:** Parse RFP/RFI requirements, score coverage, identify gaps, and generate bid/no-bid recommendations.
**Coverage Categories:** Full (100%), Partial (50%), Planned (25%), Gap (0%).
**Priority Weighting:** Must-Have 3×, Should-Have 2×, Nice-to-Have 1×.
**Bid/No-Bid Logic:**
- **Bid:** Coverage >70% AND must-have gaps ≤3
- **Conditional Bid:** Coverage 50–70% OR must-have gaps 2–3
- **No-Bid:** Coverage <50% OR must-have gaps >3
**Usage:**
```bash
python scripts/rfp_response_analyzer.py assets/sample_rfp_data.json # human-readable
python scripts/rfp_response_analyzer.py assets/sample_rfp_data.json --format json # JSON output
python scripts/rfp_response_analyzer.py --help
```
**Input Format:** See `assets/sample_rfp_data.json` for the complete schema.
---
### 2. Competitive Matrix Builder
**Script:** `scripts/competitive_matrix_builder.py`
**Purpose:** Generate feature comparison matrices, calculate competitive scores, identify differentiators and vulnerabilities.
**Feature Scoring:** Full (3), Partial (2), Limited (1), None (0).
**Usage:**
```bash
python scripts/competitive_matrix_builder.py competitive_data.json # human-readable
python scripts/competitive_matrix_builder.py competitive_data.json --format json # JSON output
```
**Output Includes:** Feature comparison matrix, weighted competitive scores, differentiators, vulnerabilities, and win themes.
---
### 3. POC Planner
**Script:** `scripts/poc_planner.py`
**Purpose:** Generate structured POC plans with timeline, resource allocation, success criteria, and evaluation scorecards.
**Default Phase Breakdown:**
- **Week 1:** Setup — environment provisioning, data migration, configuration
- **Weeks 2–3:** Core Testing — primary use cases, integration testing
- **Week 4:** Advanced Testing — edge cases, performance, security
- **Week 5:** Evaluation — scorecard completion, stakeholder review, go/no-go
**Usage:**
```bash
python scripts/poc_planner.py poc_data.json # human-readable
python scripts/poc_planner.py poc_data.json --format json # JSON output
```
**Output Includes:** Phased POC plan, resource allocation, success criteria, evaluation scorecard, risk register, and go/no-go recommendation framework.
---
## Reference Knowledge Bases
| Reference | Description |
|-----------|-------------|
| `references/rfp-response-guide.md` | RFP/RFI response best practices, compliance matrix, bid/no-bid framework |
| `references/competitive-positioning-framework.md` | Competitive analysis methodology, battlecard creation, objection handling |
| `references/poc-best-practices.md` | POC planning methodology, success criteria, evaluation frameworks |
## Asset Templates
| Template | Purpose |
|----------|---------|
| `assets/technical_proposal_template.md` | Technical proposal with executive summary, solution architecture, implementation plan |
| `assets/demo_script_template.md` | Demo script with agenda, talking points, objection handling |
| `assets/poc_scorecard_template.md` | POC evaluation scorecard with weighted scoring |
| `assets/sample_rfp_data.json` | Sample RFP data for testing the analyzer |
| `assets/expected_output.json` | Expected output from rfp_response_analyzer.py |
## Integration Points
- **Marketing Skills** - Leverage competitive intelligence and messaging frameworks from `../../marketing-skill/`
- **Product Team** - Coordinate on roadmap items flagged as "Planned" in RFP analysis from `../../product-team/`
- **C-Level Advisory** - Escalate strategic deals requiring executive engagement from `../../c-level-advisor/`
- **Customer Success** - Hand off POC results and success criteria to CSM from `../customer-success-manager/`
---
**Last Updated:** February 2026
**Status:** Production-ready
**Tools:** 3 Python automation scripts
**References:** 3 knowledge base documents
**Templates:** 5 asset files
FILE:assets/demo_script_template.md
# Demo Script Template
## Demo Information
| Field | Value |
|-------|-------|
| Customer | [Customer Name] |
| Date/Time | [Date and Time] |
| Duration | [XX minutes] |
| Demo Environment | [Environment URL/Details] |
| Presenter | [Sales Engineer Name] |
| AE/Account Executive | [AE Name] |
---
## Pre-Demo Checklist
- [ ] Demo environment tested and confirmed working
- [ ] Sample data loaded and validated
- [ ] Backup demo environment prepared
- [ ] Screen sharing tested with correct resolution
- [ ] Browser tabs pre-loaded with key screens
- [ ] Recording setup confirmed (if applicable)
- [ ] Customer-specific branding applied (if applicable)
- [ ] Network and VPN connectivity verified
- [ ] All integrations connected and tested
- [ ] Backup slides prepared in case of technical issues
---
## Attendees and Roles
| Name | Title | Role in Evaluation | Key Interest |
|------|-------|-------------------|--------------|
| [Name] | [CTO/VP Eng] | Decision Maker | ROI, strategic fit |
| [Name] | [Director] | Champion | Solving [specific problem] |
| [Name] | [Manager] | Technical Evaluator | Architecture, integrations |
| [Name] | [Analyst] | End User | Day-to-day usability |
---
## Agenda
| Time | Duration | Topic | Lead |
|------|----------|-------|------|
| 0:00 | 5 min | Welcome and introductions | AE |
| 0:05 | 5 min | Agenda and objectives | SE |
| 0:10 | 20 min | Core demo (Use Cases 1-3) | SE |
| 0:30 | 10 min | Integration demo | SE |
| 0:40 | 5 min | Admin and security overview | SE |
| 0:45 | 10 min | Q&A | SE + AE |
| 0:55 | 5 min | Next steps and wrap-up | AE |
---
## Demo Flow
### Opening (5 minutes)
**Talking Points:**
- Thank attendees for their time
- Recap what we learned in discovery: "[Summarize 2-3 key challenges]"
- Set expectations: "Today I'll show you how we address [Challenge 1], [Challenge 2], and [Challenge 3]"
- Frame the demo: "I'll be using [data type] similar to what you described in our earlier conversations"
**Transition:** "Let me start with the challenge you mentioned is most pressing: [Challenge 1]."
---
### Use Case 1: [Name] (7 minutes)
**Business Context:**
[1-2 sentences on why this matters to the customer]
**Demo Steps:**
1. **Step 1:** [Navigate to / Click on / Show...]
- **What to say:** "[Explain what they're seeing and why it matters]"
- **Highlight:** [Specific feature or capability to emphasize]
2. **Step 2:** [Navigate to / Click on / Show...]
- **What to say:** "[Connect this to their specific pain point]"
- **Highlight:** [Differentiator from competitor]
3. **Step 3:** [Navigate to / Click on / Show...]
- **What to say:** "[Quantify the value - time saved, errors reduced, etc.]"
- **Highlight:** [Ease of use or power of the feature]
**Key Message:** "[One sentence summarizing the value demonstrated]"
**Transition:** "Now that you've seen how we handle [Use Case 1], let me show you [Use Case 2]."
---
### Use Case 2: [Name] (7 minutes)
**Business Context:**
[1-2 sentences on why this matters to the customer]
**Demo Steps:**
1. **Step 1:** [Navigate to / Click on / Show...]
- **What to say:** "[Explanation]"
- **Highlight:** [Key capability]
2. **Step 2:** [Navigate to / Click on / Show...]
- **What to say:** "[Explanation]"
- **Highlight:** [Key capability]
3. **Step 3:** [Navigate to / Click on / Show...]
- **What to say:** "[Explanation]"
- **Highlight:** [Key capability]
**Key Message:** "[One sentence summarizing the value demonstrated]"
**Transition:** "[Transition statement to next section]"
---
### Use Case 3: [Name] (6 minutes)
**Business Context:**
[1-2 sentences on why this matters to the customer]
**Demo Steps:**
1. **Step 1:** [Description]
- **What to say:** "[Explanation]"
- **Highlight:** [Key capability]
2. **Step 2:** [Description]
- **What to say:** "[Explanation]"
- **Highlight:** [Key capability]
**Key Message:** "[One sentence summarizing the value demonstrated]"
---
### Integration Demo (10 minutes)
**Context:** "You mentioned that integration with [System X] and [System Y] is critical. Let me show you how that works."
**Demo Steps:**
1. **Show integration configuration:**
- **What to say:** "Setting up the connection takes [X minutes/clicks]"
- **Highlight:** Native connector, no custom code required
2. **Show data flow:**
- **What to say:** "Data syncs in [real-time/X minute intervals]"
- **Highlight:** Reliability, error handling, monitoring
3. **Show end-to-end workflow:**
- **What to say:** "Here's the complete flow from [source] to [destination]"
- **Highlight:** Automation, reduced manual effort
---
### Admin and Security (5 minutes)
**Demo Steps:**
1. **Show RBAC configuration:**
- **What to say:** "Administrators can define roles and permissions at [granularity level]"
2. **Show audit log:**
- **What to say:** "Every action is logged for compliance and security review"
3. **Show SSO setup:**
- **What to say:** "Single sign-on integrates with your existing identity provider"
---
## Objection Handling
### Anticipated Objections
| Objection | Response |
|-----------|----------|
| "[Feature X] looks limited compared to [Competitor]" | "Great observation. Our approach to [Feature X] focuses on [benefit]. What specific aspect of [Feature X] is most important to your workflow? [Then demonstrate or explain how we address the specific need]" |
| "How does this handle [edge case]?" | "That's an important scenario. [If supported: Let me show you how that works.] [If not directly: Here's how our customers typically handle that use case...]" |
| "What about performance at our scale?" | "Excellent question. Our platform handles [benchmark data]. For your specific scale of [X], we'd recommend [architecture approach]. We can validate this in a POC." |
| "The implementation timeline seems long" | "The timeline I shared is for the full solution. We can phase the rollout to deliver value sooner. Phase 1 would give you [core capability] within [X weeks]." |
| "What happens if we outgrow this?" | "Our architecture is designed for growth. [Describe scaling approach]. We have customers who have scaled from [X] to [Y] without re-architecture." |
### Recovery Strategies
**If the demo breaks:**
1. Stay calm: "Let me switch to [backup environment / backup approach]"
2. Explain what they would have seen
3. Offer to follow up with a recorded walkthrough
4. Pivot to the next demo section
**If an unexpected question derails the flow:**
1. Acknowledge: "That's an excellent question"
2. Briefly answer or note it for follow-up
3. Return to the demo flow: "Let me continue with [next section] and we can dive deeper into that during Q&A"
**If the audience seems disengaged:**
1. Pause and ask: "Before I continue, is this addressing what you're looking for?"
2. Adjust focus based on their response
3. Skip ahead to the section most relevant to their interests
---
## Post-Demo Actions
- [ ] Send thank-you email with recording link (if recorded)
- [ ] Share demo environment access credentials (if applicable)
- [ ] Send follow-up document addressing unanswered questions
- [ ] Schedule next meeting (POC kickoff, technical deep-dive, etc.)
- [ ] Update CRM with demo notes and next steps
- [ ] Debrief with AE on stakeholder reactions and concerns
- [ ] Log key objections and responses for battlecard updates
---
## Notes
[Space for real-time notes during the demo]
### Questions Raised
1. [Question] - [Answer / Follow-up needed]
2. [Question] - [Answer / Follow-up needed]
### Feedback Received
- [Positive feedback]
- [Concerns raised]
### Next Steps Agreed
1. [Action item] - [Owner] - [Date]
2. [Action item] - [Owner] - [Date]
FILE:assets/expected_output.json
{
"rfp_info": {
"rfp_name": "Enterprise Data Analytics Platform RFP",
"customer": "Acme Financial Services",
"due_date": "2026-03-15",
"strategic_value": "high",
"deal_value": "$450,000 ARR"
},
"coverage_summary": {
"overall_coverage_percentage": 84.5,
"total_requirements": 21,
"full": 14,
"partial": 3,
"planned": 2,
"gap": 2,
"must_have_gaps": 0
},
"category_scores": {
"Data Integration": {
"coverage_percentage": 90.0,
"requirements_count": 4,
"full": 3,
"partial": 1,
"planned": 0,
"gap": 0,
"effort_hours": 34
},
"Analytics & Visualization": {
"coverage_percentage": 77.8,
"requirements_count": 4,
"full": 2,
"partial": 1,
"planned": 1,
"gap": 0,
"effort_hours": 56
},
"Security & Compliance": {
"coverage_percentage": 81.8,
"requirements_count": 4,
"full": 3,
"partial": 0,
"planned": 0,
"gap": 1,
"effort_hours": 50
},
"Performance & Scalability": {
"coverage_percentage": 87.5,
"requirements_count": 3,
"full": 2,
"partial": 1,
"planned": 0,
"gap": 0,
"effort_hours": 32
},
"API & Extensibility": {
"coverage_percentage": 87.5,
"requirements_count": 3,
"full": 2,
"partial": 0,
"planned": 1,
"gap": 0,
"effort_hours": 38
},
"Support & SLA": {
"coverage_percentage": 100.0,
"requirements_count": 2,
"full": 2,
"partial": 0,
"planned": 0,
"gap": 0,
"effort_hours": 4
},
"Deployment": {
"coverage_percentage": 0.0,
"requirements_count": 1,
"full": 0,
"partial": 0,
"planned": 0,
"gap": 1,
"effort_hours": 80
}
},
"bid_recommendation": {
"decision": "BID",
"confidence": "high",
"overall_coverage_percentage": 84.5,
"must_have_gaps": 0,
"strategic_value": "high",
"reasons": [
"Coverage score 84.5% exceeds 70% threshold"
]
},
"gap_analysis": [
{
"id": "R-004",
"requirement": "Change data capture (CDC) for real-time sync",
"category": "Data Integration",
"priority": "should-have",
"coverage_status": "partial",
"severity": "high",
"effort_hours": 16,
"mitigation": "Document supported CDC sources; provide configuration guide for non-standard sources"
},
{
"id": "R-007",
"requirement": "Natural language query interface for business users",
"category": "Analytics & Visualization",
"priority": "should-have",
"coverage_status": "planned",
"severity": "high",
"effort_hours": 24,
"mitigation": "Share roadmap timeline; offer guided query builder as interim solution"
},
{
"id": "R-012",
"requirement": "HIPAA compliance for healthcare data handling",
"category": "Security & Compliance",
"priority": "should-have",
"coverage_status": "gap",
"severity": "high",
"effort_hours": 40,
"mitigation": "Evaluate HIPAA certification timeline with compliance team; consider data masking as interim"
},
{
"id": "R-015",
"requirement": "Multi-region deployment with data residency controls",
"category": "Performance & Scalability",
"priority": "should-have",
"coverage_status": "partial",
"severity": "high",
"effort_hours": 20,
"mitigation": "Confirm customer region requirements; provide APAC beta access if needed"
},
{
"id": "R-008",
"requirement": "Predictive analytics and ML model integration",
"category": "Analytics & Visualization",
"priority": "nice-to-have",
"coverage_status": "partial",
"severity": "low",
"effort_hours": 20,
"mitigation": "Demonstrate Python integration for custom models; provide example notebooks"
},
{
"id": "R-018",
"requirement": "Custom plugin/extension framework",
"category": "API & Extensibility",
"priority": "nice-to-have",
"coverage_status": "planned",
"severity": "low",
"effort_hours": 30,
"mitigation": "Current API extensibility covers most use cases; plugin framework will expand options"
},
{
"id": "R-021",
"requirement": "On-premise deployment option",
"category": "Deployment",
"priority": "nice-to-have",
"coverage_status": "gap",
"severity": "low",
"effort_hours": 80,
"mitigation": "Position cloud-first architecture benefits; offer VPC deployment as alternative"
}
],
"risk_assessment": [
{
"risk": "High customization effort",
"impact": "high",
"description": "230 hours estimated for non-full requirements",
"mitigation": "Evaluate resource availability and timeline feasibility before committing"
}
],
"effort_estimate": {
"total_hours": 294,
"gap_closure_hours": 230,
"full_coverage_hours": 64
},
"requirements_detail": [
{
"id": "R-001",
"requirement": "Real-time data ingestion from multiple sources (APIs, databases, streaming)",
"category": "Data Integration",
"priority": "must-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 3.0,
"weighted_score": 3.0,
"max_weighted": 3.0,
"effort_hours": 8,
"notes": "Native connectors for 200+ data sources",
"mitigation": ""
},
{
"id": "R-002",
"requirement": "Support for SQL and NoSQL data sources",
"category": "Data Integration",
"priority": "must-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 3.0,
"weighted_score": 3.0,
"max_weighted": 3.0,
"effort_hours": 4,
"notes": "Supports PostgreSQL, MySQL, MongoDB, Cassandra, and more",
"mitigation": ""
},
{
"id": "R-003",
"requirement": "Automated ETL pipeline creation with visual designer",
"category": "Data Integration",
"priority": "should-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 2.0,
"weighted_score": 2.0,
"max_weighted": 2.0,
"effort_hours": 6,
"notes": "Drag-and-drop pipeline builder included",
"mitigation": ""
},
{
"id": "R-004",
"requirement": "Change data capture (CDC) for real-time sync",
"category": "Data Integration",
"priority": "should-have",
"coverage_status": "partial",
"coverage_score": 0.5,
"weight": 2.0,
"weighted_score": 1.0,
"max_weighted": 2.0,
"effort_hours": 16,
"notes": "CDC supported for major databases; some require custom configuration",
"mitigation": "Document supported CDC sources; provide configuration guide for non-standard sources"
},
{
"id": "R-005",
"requirement": "Interactive dashboard creation with drag-and-drop",
"category": "Analytics & Visualization",
"priority": "must-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 3.0,
"weighted_score": 3.0,
"max_weighted": 3.0,
"effort_hours": 4,
"notes": "Full drag-and-drop dashboard builder with 50+ chart types",
"mitigation": ""
},
{
"id": "R-006",
"requirement": "Embedded analytics with white-labeling support",
"category": "Analytics & Visualization",
"priority": "must-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 3.0,
"weighted_score": 3.0,
"max_weighted": 3.0,
"effort_hours": 8,
"notes": "Full embedding SDK with CSS customization",
"mitigation": ""
},
{
"id": "R-007",
"requirement": "Natural language query interface for business users",
"category": "Analytics & Visualization",
"priority": "should-have",
"coverage_status": "planned",
"coverage_score": 0.25,
"weight": 2.0,
"weighted_score": 0.5,
"max_weighted": 2.0,
"effort_hours": 24,
"notes": "NLQ feature on roadmap for Q3 2026",
"mitigation": "Share roadmap timeline; offer guided query builder as interim solution"
},
{
"id": "R-008",
"requirement": "Predictive analytics and ML model integration",
"category": "Analytics & Visualization",
"priority": "nice-to-have",
"coverage_status": "partial",
"coverage_score": 0.5,
"weight": 1.0,
"weighted_score": 0.5,
"max_weighted": 1.0,
"effort_hours": 20,
"notes": "Python/R integration available; no built-in ML models",
"mitigation": "Demonstrate Python integration for custom models; provide example notebooks"
},
{
"id": "R-009",
"requirement": "Role-based access control (RBAC) with row-level security",
"category": "Security & Compliance",
"priority": "must-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 3.0,
"weighted_score": 3.0,
"max_weighted": 3.0,
"effort_hours": 6,
"notes": "Granular RBAC with row-level and column-level security",
"mitigation": ""
},
{
"id": "R-010",
"requirement": "SOC 2 Type II certification",
"category": "Security & Compliance",
"priority": "must-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 3.0,
"weighted_score": 3.0,
"max_weighted": 3.0,
"effort_hours": 2,
"notes": "Current SOC 2 Type II report available upon NDA",
"mitigation": ""
},
{
"id": "R-011",
"requirement": "Data encryption at rest and in transit (AES-256, TLS 1.3)",
"category": "Security & Compliance",
"priority": "must-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 3.0,
"weighted_score": 3.0,
"max_weighted": 3.0,
"effort_hours": 2,
"notes": "AES-256 at rest, TLS 1.3 in transit, customer-managed keys supported",
"mitigation": ""
},
{
"id": "R-012",
"requirement": "HIPAA compliance for healthcare data handling",
"category": "Security & Compliance",
"priority": "should-have",
"coverage_status": "gap",
"coverage_score": 0.0,
"weight": 2.0,
"weighted_score": 0.0,
"max_weighted": 2.0,
"effort_hours": 40,
"notes": "HIPAA BAA not currently offered",
"mitigation": "Evaluate HIPAA certification timeline with compliance team; consider data masking as interim"
},
{
"id": "R-013",
"requirement": "Horizontal scaling to handle 10B+ rows",
"category": "Performance & Scalability",
"priority": "must-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 3.0,
"weighted_score": 3.0,
"max_weighted": 3.0,
"effort_hours": 8,
"notes": "Distributed query engine scales to 50B+ rows",
"mitigation": ""
},
{
"id": "R-014",
"requirement": "Sub-second query response for cached dashboards",
"category": "Performance & Scalability",
"priority": "must-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 3.0,
"weighted_score": 3.0,
"max_weighted": 3.0,
"effort_hours": 4,
"notes": "Intelligent caching layer with <500ms p95 for cached queries",
"mitigation": ""
},
{
"id": "R-015",
"requirement": "Multi-region deployment with data residency controls",
"category": "Performance & Scalability",
"priority": "should-have",
"coverage_status": "partial",
"coverage_score": 0.5,
"weight": 2.0,
"weighted_score": 1.0,
"max_weighted": 2.0,
"effort_hours": 20,
"notes": "US and EU regions available; APAC region in beta",
"mitigation": "Confirm customer region requirements; provide APAC beta access if needed"
},
{
"id": "R-016",
"requirement": "RESTful API with comprehensive documentation",
"category": "API & Extensibility",
"priority": "must-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 3.0,
"weighted_score": 3.0,
"max_weighted": 3.0,
"effort_hours": 4,
"notes": "Full REST API with OpenAPI spec and interactive documentation",
"mitigation": ""
},
{
"id": "R-017",
"requirement": "Webhook support for event-driven workflows",
"category": "API & Extensibility",
"priority": "should-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 2.0,
"weighted_score": 2.0,
"max_weighted": 2.0,
"effort_hours": 4,
"notes": "Webhook support for 30+ event types",
"mitigation": ""
},
{
"id": "R-018",
"requirement": "Custom plugin/extension framework",
"category": "API & Extensibility",
"priority": "nice-to-have",
"coverage_status": "planned",
"coverage_score": 0.25,
"weight": 1.0,
"weighted_score": 0.25,
"max_weighted": 1.0,
"effort_hours": 30,
"notes": "Plugin framework on roadmap for Q4 2026",
"mitigation": "Current API extensibility covers most use cases; plugin framework will expand options"
},
{
"id": "R-019",
"requirement": "24/7 enterprise support with 1-hour critical response time",
"category": "Support & SLA",
"priority": "must-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 3.0,
"weighted_score": 3.0,
"max_weighted": 3.0,
"effort_hours": 2,
"notes": "Premium support tier includes 24/7 coverage with 30-min critical response SLA",
"mitigation": ""
},
{
"id": "R-020",
"requirement": "Dedicated customer success manager",
"category": "Support & SLA",
"priority": "should-have",
"coverage_status": "full",
"coverage_score": 1.0,
"weight": 2.0,
"weighted_score": 2.0,
"max_weighted": 2.0,
"effort_hours": 2,
"notes": "Included in Enterprise tier",
"mitigation": ""
},
{
"id": "R-021",
"requirement": "On-premise deployment option",
"category": "Deployment",
"priority": "nice-to-have",
"coverage_status": "gap",
"coverage_score": 0.0,
"weight": 1.0,
"weighted_score": 0.0,
"max_weighted": 1.0,
"effort_hours": 80,
"notes": "Cloud-only platform; no on-premise offering",
"mitigation": "Position cloud-first architecture benefits; offer VPC deployment as alternative"
}
]
}
FILE:assets/poc_scorecard_template.md
# POC Evaluation Scorecard
## Scorecard Information
| Field | Value |
|-------|-------|
| POC Name | [POC Name] |
| Customer | [Customer Name] |
| Vendor/Product | [Product Name] |
| Evaluation Period | [Start Date] - [End Date] |
| Evaluated By | [Names and Roles] |
| Date Completed | [Date] |
---
## Scoring Scale
| Score | Label | Definition |
|-------|-------|------------|
| 5 | Exceeds | Superior capability; exceeds requirements with notable strengths |
| 4 | Meets | Full capability; meets all requirements with no significant gaps |
| 3 | Partial | Acceptable capability; minor gaps that can be addressed |
| 2 | Below | Below expectations; significant gaps that impact value |
| 1 | Fails | Does not meet requirements; critical gaps |
| N/A | Not Evaluated | Not tested during this POC |
---
## Evaluation Categories
### 1. Functionality (Weight: 30%)
| Criterion | Score (1-5) | Evidence / Notes |
|-----------|-------------|-----------------|
| Core feature completeness | | |
| Use case coverage | | |
| Customization flexibility | | |
| Workflow automation | | |
| Data handling and transformation | | |
| Reporting and analytics | | |
**Category Score:** ___/5.0
**Category Notes:**
[Summary of functionality evaluation, key strengths and gaps]
---
### 2. Performance (Weight: 20%)
| Criterion | Score (1-5) | Evidence / Notes |
|-----------|-------------|-----------------|
| Response time under expected load | | |
| Response time under peak load | | |
| Throughput capacity | | |
| Scalability characteristics | | |
| Resource utilization | | |
| Batch processing performance | | |
**Category Score:** ___/5.0
**Category Notes:**
[Summary of performance evaluation, benchmark results]
---
### 3. Integration (Weight: 20%)
| Criterion | Score (1-5) | Evidence / Notes |
|-----------|-------------|-----------------|
| API completeness and documentation | | |
| Data migration ease | | |
| Third-party connector availability | | |
| Authentication/SSO integration | | |
| Real-time sync reliability | | |
| Error handling and recovery | | |
**Category Score:** ___/5.0
**Category Notes:**
[Summary of integration evaluation, systems tested]
---
### 4. Usability (Weight: 15%)
| Criterion | Score (1-5) | Evidence / Notes |
|-----------|-------------|-----------------|
| User interface intuitiveness | | |
| Learning curve assessment | | |
| Documentation quality | | |
| Admin console functionality | | |
| Mobile experience | | |
| Accessibility compliance | | |
**Category Score:** ___/5.0
**Category Notes:**
[Summary of usability evaluation, user feedback]
---
### 5. Support (Weight: 15%)
| Criterion | Score (1-5) | Evidence / Notes |
|-----------|-------------|-----------------|
| Technical support responsiveness | | |
| Knowledge base quality | | |
| Training resources availability | | |
| Community and ecosystem | | |
| Issue resolution speed | | |
| Proactive engagement quality | | |
**Category Score:** ___/5.0
**Category Notes:**
[Summary of support evaluation during POC]
---
## Score Summary
| Category | Weight | Score | Weighted Score |
|----------|--------|-------|----------------|
| Functionality | 30% | ___/5.0 | ___ |
| Performance | 20% | ___/5.0 | ___ |
| Integration | 20% | ___/5.0 | ___ |
| Usability | 15% | ___/5.0 | ___ |
| Support | 15% | ___/5.0 | ___ |
| **Overall** | **100%** | | **___/5.0** |
### Decision Thresholds
| Weighted Average | Decision |
|-----------------|----------|
| >= 4.0 | **Strong Pass** - Proceed to procurement |
| 3.5 - 3.9 | **Pass** - Proceed with noted conditions |
| 3.0 - 3.4 | **Conditional** - Requires further evaluation |
| < 3.0 | **Fail** - Does not meet requirements |
---
## Success Criteria Results
| # | Criterion | Priority | Target | Actual | Pass/Fail |
|---|-----------|----------|--------|--------|-----------|
| 1 | [Criterion 1] | Must-Have | [Target] | [Result] | [ ] |
| 2 | [Criterion 2] | Must-Have | [Target] | [Result] | [ ] |
| 3 | [Criterion 3] | Must-Have | [Target] | [Result] | [ ] |
| 4 | [Criterion 4] | Should-Have | [Target] | [Result] | [ ] |
| 5 | [Criterion 5] | Should-Have | [Target] | [Result] | [ ] |
| 6 | [Criterion 6] | Nice-to-Have | [Target] | [Result] | [ ] |
**Must-Have Pass Rate:** ___/%
**Overall Pass Rate:** ___/%
---
## Issues Log
| # | Issue | Severity | Status | Resolution | Impact on Score |
|---|-------|----------|--------|------------|----------------|
| 1 | [Issue] | [Critical/High/Medium/Low] | [Open/Resolved] | [Resolution] | [Category affected] |
| 2 | [Issue] | [Critical/High/Medium/Low] | [Open/Resolved] | [Resolution] | [Category affected] |
---
## Stakeholder Feedback
### [Stakeholder Name 1] - [Role]
**Rating:** ___/5
**Comments:** [Feedback]
### [Stakeholder Name 2] - [Role]
**Rating:** ___/5
**Comments:** [Feedback]
### [Stakeholder Name 3] - [Role]
**Rating:** ___/5
**Comments:** [Feedback]
---
## Recommendation
### Decision: [ ] GO / [ ] CONDITIONAL GO / [ ] NO-GO
**Rationale:**
[2-3 paragraphs explaining the recommendation based on scorecard results, success criteria outcomes, stakeholder feedback, and overall evaluation]
**Conditions (if Conditional GO):**
1. [Condition 1 that must be met before proceeding]
2. [Condition 2 that must be met before proceeding]
**Key Strengths:**
1. [Strength 1]
2. [Strength 2]
3. [Strength 3]
**Key Concerns:**
1. [Concern 1 with proposed mitigation]
2. [Concern 2 with proposed mitigation]
**Next Steps:**
1. [Action item] - [Owner] - [Date]
2. [Action item] - [Owner] - [Date]
3. [Action item] - [Owner] - [Date]
---
## Sign-Off
| Role | Name | Signature | Date |
|------|------|-----------|------|
| Technical Evaluator | | | |
| Business Sponsor | | | |
| Decision Maker | | | |
| Sales Engineer | | | |
FILE:assets/sample_rfp_data.json
{
"rfp_name": "Enterprise Data Analytics Platform RFP",
"customer": "Acme Financial Services",
"due_date": "2026-03-15",
"deal_value": "$450,000 ARR",
"strategic_value": "high",
"requirements": [
{
"id": "R-001",
"requirement": "Real-time data ingestion from multiple sources (APIs, databases, streaming)",
"category": "Data Integration",
"priority": "must-have",
"coverage_status": "full",
"effort_hours": 8,
"notes": "Native connectors for 200+ data sources",
"mitigation": ""
},
{
"id": "R-002",
"requirement": "Support for SQL and NoSQL data sources",
"category": "Data Integration",
"priority": "must-have",
"coverage_status": "full",
"effort_hours": 4,
"notes": "Supports PostgreSQL, MySQL, MongoDB, Cassandra, and more",
"mitigation": ""
},
{
"id": "R-003",
"requirement": "Automated ETL pipeline creation with visual designer",
"category": "Data Integration",
"priority": "should-have",
"coverage_status": "full",
"effort_hours": 6,
"notes": "Drag-and-drop pipeline builder included",
"mitigation": ""
},
{
"id": "R-004",
"requirement": "Change data capture (CDC) for real-time sync",
"category": "Data Integration",
"priority": "should-have",
"coverage_status": "partial",
"effort_hours": 16,
"notes": "CDC supported for major databases; some require custom configuration",
"mitigation": "Document supported CDC sources; provide configuration guide for non-standard sources"
},
{
"id": "R-005",
"requirement": "Interactive dashboard creation with drag-and-drop",
"category": "Analytics & Visualization",
"priority": "must-have",
"coverage_status": "full",
"effort_hours": 4,
"notes": "Full drag-and-drop dashboard builder with 50+ chart types",
"mitigation": ""
},
{
"id": "R-006",
"requirement": "Embedded analytics with white-labeling support",
"category": "Analytics & Visualization",
"priority": "must-have",
"coverage_status": "full",
"effort_hours": 8,
"notes": "Full embedding SDK with CSS customization",
"mitigation": ""
},
{
"id": "R-007",
"requirement": "Natural language query interface for business users",
"category": "Analytics & Visualization",
"priority": "should-have",
"coverage_status": "planned",
"effort_hours": 24,
"notes": "NLQ feature on roadmap for Q3 2026",
"mitigation": "Share roadmap timeline; offer guided query builder as interim solution"
},
{
"id": "R-008",
"requirement": "Predictive analytics and ML model integration",
"category": "Analytics & Visualization",
"priority": "nice-to-have",
"coverage_status": "partial",
"effort_hours": 20,
"notes": "Python/R integration available; no built-in ML models",
"mitigation": "Demonstrate Python integration for custom models; provide example notebooks"
},
{
"id": "R-009",
"requirement": "Role-based access control (RBAC) with row-level security",
"category": "Security & Compliance",
"priority": "must-have",
"coverage_status": "full",
"effort_hours": 6,
"notes": "Granular RBAC with row-level and column-level security",
"mitigation": ""
},
{
"id": "R-010",
"requirement": "SOC 2 Type II certification",
"category": "Security & Compliance",
"priority": "must-have",
"coverage_status": "full",
"effort_hours": 2,
"notes": "Current SOC 2 Type II report available upon NDA",
"mitigation": ""
},
{
"id": "R-011",
"requirement": "Data encryption at rest and in transit (AES-256, TLS 1.3)",
"category": "Security & Compliance",
"priority": "must-have",
"coverage_status": "full",
"effort_hours": 2,
"notes": "AES-256 at rest, TLS 1.3 in transit, customer-managed keys supported",
"mitigation": ""
},
{
"id": "R-012",
"requirement": "HIPAA compliance for healthcare data handling",
"category": "Security & Compliance",
"priority": "should-have",
"coverage_status": "gap",
"effort_hours": 40,
"notes": "HIPAA BAA not currently offered",
"mitigation": "Evaluate HIPAA certification timeline with compliance team; consider data masking as interim"
},
{
"id": "R-013",
"requirement": "Horizontal scaling to handle 10B+ rows",
"category": "Performance & Scalability",
"priority": "must-have",
"coverage_status": "full",
"effort_hours": 8,
"notes": "Distributed query engine scales to 50B+ rows",
"mitigation": ""
},
{
"id": "R-014",
"requirement": "Sub-second query response for cached dashboards",
"category": "Performance & Scalability",
"priority": "must-have",
"coverage_status": "full",
"effort_hours": 4,
"notes": "Intelligent caching layer with <500ms p95 for cached queries",
"mitigation": ""
},
{
"id": "R-015",
"requirement": "Multi-region deployment with data residency controls",
"category": "Performance & Scalability",
"priority": "should-have",
"coverage_status": "partial",
"effort_hours": 20,
"notes": "US and EU regions available; APAC region in beta",
"mitigation": "Confirm customer region requirements; provide APAC beta access if needed"
},
{
"id": "R-016",
"requirement": "RESTful API with comprehensive documentation",
"category": "API & Extensibility",
"priority": "must-have",
"coverage_status": "full",
"effort_hours": 4,
"notes": "Full REST API with OpenAPI spec and interactive documentation",
"mitigation": ""
},
{
"id": "R-017",
"requirement": "Webhook support for event-driven workflows",
"category": "API & Extensibility",
"priority": "should-have",
"coverage_status": "full",
"effort_hours": 4,
"notes": "Webhook support for 30+ event types",
"mitigation": ""
},
{
"id": "R-018",
"requirement": "Custom plugin/extension framework",
"category": "API & Extensibility",
"priority": "nice-to-have",
"coverage_status": "planned",
"effort_hours": 30,
"notes": "Plugin framework on roadmap for Q4 2026",
"mitigation": "Current API extensibility covers most use cases; plugin framework will expand options"
},
{
"id": "R-019",
"requirement": "24/7 enterprise support with 1-hour critical response time",
"category": "Support & SLA",
"priority": "must-have",
"coverage_status": "full",
"effort_hours": 2,
"notes": "Premium support tier includes 24/7 coverage with 30-min critical response SLA",
"mitigation": ""
},
{
"id": "R-020",
"requirement": "Dedicated customer success manager",
"category": "Support & SLA",
"priority": "should-have",
"coverage_status": "full",
"effort_hours": 2,
"notes": "Included in Enterprise tier",
"mitigation": ""
},
{
"id": "R-021",
"requirement": "On-premise deployment option",
"category": "Deployment",
"priority": "nice-to-have",
"coverage_status": "gap",
"effort_hours": 80,
"notes": "Cloud-only platform; no on-premise offering",
"mitigation": "Position cloud-first architecture benefits; offer VPC deployment as alternative"
}
]
}
FILE:assets/technical_proposal_template.md
# Technical Proposal Template
## Document Information
| Field | Value |
|-------|-------|
| Customer | [Customer Name] |
| Opportunity | [Opportunity Name / RFP Reference] |
| Prepared By | [Sales Engineer Name] |
| Date | [Date] |
| Version | [Version Number] |
| Classification | [Confidential / Internal] |
---
## 1. Executive Summary
### Business Context
[2-3 paragraphs summarizing the customer's business challenges and strategic objectives that this solution addresses. Focus on business outcomes, not technical features.]
### Proposed Solution
[1-2 paragraphs describing the solution at a high level, emphasizing how it addresses the specific challenges identified above.]
### Key Value Propositions
1. **[Value 1]:** [Quantified benefit, e.g., "Reduce reporting time by 60%"]
2. **[Value 2]:** [Quantified benefit]
3. **[Value 3]:** [Quantified benefit]
### Recommended Approach
[Brief overview of the implementation approach, timeline, and key milestones.]
---
## 2. Requirements Summary
### Coverage Overview
| Category | Requirements | Full | Partial | Planned | Gap | Coverage |
|----------|-------------|------|---------|---------|-----|----------|
| [Category 1] | [N] | [N] | [N] | [N] | [N] | [X%] |
| [Category 2] | [N] | [N] | [N] | [N] | [N] | [X%] |
| **Total** | **[N]** | **[N]** | **[N]** | **[N]** | **[N]** | **[X%]** |
### Key Differentiators
1. [Differentiator 1 with brief explanation]
2. [Differentiator 2 with brief explanation]
3. [Differentiator 3 with brief explanation]
### Gap Mitigation Plan
| Gap | Priority | Mitigation Strategy | Timeline |
|-----|----------|-------------------|----------|
| [Gap 1] | [Must/Should/Nice] | [Strategy] | [Date] |
| [Gap 2] | [Must/Should/Nice] | [Strategy] | [Date] |
---
## 3. Solution Architecture
### Architecture Overview
[High-level architecture description. Include or reference an architecture diagram.]
```
[ASCII architecture diagram or reference to attached diagram]
Example:
+------------------+ +------------------+ +------------------+
| Data Sources | --> | Our Platform | --> | Delivery |
| - System A | | - Ingestion | | - Dashboards |
| - System B | | - Processing | | - API |
| - System C | | - Analytics | | - Exports |
+------------------+ +------------------+ +------------------+
|
+------------------+
| Management |
| - Security |
| - Monitoring |
| - Admin |
+------------------+
```
### Component Details
#### [Component 1]
- **Purpose:** [What this component does]
- **Technology:** [Underlying technology]
- **Scaling:** [How it scales]
- **Availability:** [HA/DR approach]
#### [Component 2]
- **Purpose:** [What this component does]
- **Technology:** [Underlying technology]
- **Scaling:** [How it scales]
- **Availability:** [HA/DR approach]
### Integration Architecture
| Integration Point | Protocol | Direction | Frequency | Authentication |
|-------------------|----------|-----------|-----------|---------------|
| [System A] | REST API | Inbound | Real-time | OAuth 2.0 |
| [System B] | JDBC | Inbound | Batch (hourly) | Service Account |
| [System C] | Webhook | Outbound | Event-driven | API Key |
### Security Architecture
- **Authentication:** [SSO, SAML, OAuth, etc.]
- **Authorization:** [RBAC, row-level security, etc.]
- **Encryption:** [At rest, in transit, key management]
- **Compliance:** [SOC 2, GDPR, HIPAA, etc.]
- **Network:** [VPC, firewall, IP restrictions]
---
## 4. Implementation Plan
### Phase Overview
| Phase | Duration | Focus | Deliverables |
|-------|----------|-------|-------------|
| Phase 1: Foundation | [X weeks] | Environment setup, core configuration | Working environment, admin access |
| Phase 2: Core Implementation | [X weeks] | Primary use cases, integrations | [Deliverables] |
| Phase 3: Advanced Features | [X weeks] | Advanced scenarios, optimization | [Deliverables] |
| Phase 4: Go-Live | [X weeks] | Testing, training, cutover | Production deployment |
### Detailed Timeline
```
Week 1-2: [Phase 1 - Foundation]
- Environment provisioning
- Security configuration
- Data source connectivity
Week 3-6: [Phase 2 - Core Implementation]
- Use case 1 implementation
- Use case 2 implementation
- Integration testing
Week 7-8: [Phase 3 - Advanced Features]
- Advanced analytics
- Custom workflows
- Performance optimization
Week 9-10: [Phase 4 - Go-Live]
- User acceptance testing
- Training sessions
- Production cutover
- Post-launch support
```
### Resource Requirements
| Role | Hours | Phase(s) | Provider |
|------|-------|----------|----------|
| Solutions Architect | [X] | All | [Vendor] |
| Implementation Engineer | [X] | 1-3 | [Vendor] |
| Project Manager | [X] | All | [Vendor] |
| Customer IT Admin | [X] | 1, 4 | [Customer] |
| Customer Business Lead | [X] | 2-4 | [Customer] |
### Training Plan
| Audience | Format | Duration | Content |
|----------|--------|----------|---------|
| Administrators | Workshop | [X hours] | Configuration, security, monitoring |
| Power Users | Workshop | [X hours] | Advanced features, reporting, automation |
| End Users | Webinar | [X hours] | Core workflows, self-service analytics |
---
## 5. Risk Mitigation
| Risk | Probability | Impact | Mitigation |
|------|------------|--------|------------|
| [Risk 1] | [H/M/L] | [H/M/L] | [Strategy] |
| [Risk 2] | [H/M/L] | [H/M/L] | [Strategy] |
| [Risk 3] | [H/M/L] | [H/M/L] | [Strategy] |
---
## 6. Commercial Summary
### Pricing Overview
| Component | Annual Cost |
|-----------|------------|
| Platform License | $[X] |
| Implementation Services | $[X] |
| Training | $[X] |
| Premium Support | $[X] |
| **Total Year 1** | **$[X]** |
| **Annual Renewal** | **$[X]** |
### ROI Projection
| Metric | Current State | With Solution | Improvement |
|--------|--------------|---------------|-------------|
| [Metric 1] | [Value] | [Value] | [%] |
| [Metric 2] | [Value] | [Value] | [%] |
| [Metric 3] | [Value] | [Value] | [%] |
**Estimated payback period:** [X months]
---
## 7. Next Steps
1. [Next step 1 with owner and date]
2. [Next step 2 with owner and date]
3. [Next step 3 with owner and date]
---
## Appendices
### A. Detailed Compliance Matrix
[Reference to full requirement-by-requirement response]
### B. Reference Customers
[2-3 relevant customer references with industry, use case, and outcomes]
### C. Architecture Diagrams
[Detailed architecture diagrams]
### D. Product Roadmap (Relevant Items)
[Roadmap items relevant to this proposal with estimated delivery dates]
FILE:references/competitive-positioning-framework.md
# Competitive Positioning Framework
A comprehensive guide for Sales Engineers to analyze competitors, build battlecards, handle objections, and position for wins.
## Competitive Analysis Methodology
### 1. Intelligence Gathering
**Primary Sources:**
- Competitor product documentation and release notes
- Analyst reports (Gartner, Forrester, IDC)
- Customer feedback from win/loss reviews
- Industry conferences and webinars
- Public case studies and testimonials
- Open-source repositories and API documentation
**Secondary Sources:**
- Glassdoor reviews (engineering culture, product direction)
- Job postings (technology stack, expansion areas)
- Patent filings (future direction signals)
- Social media and community forums
- Partner ecosystem announcements
### 2. Feature Comparison Best Practices
**Feature Scoring Scale:**
| Score | Label | Definition |
|-------|-------|------------|
| 3 | Full | Complete, production-ready feature support |
| 2 | Partial | Feature exists but with limitations or caveats |
| 1 | Limited | Minimal implementation, significant gaps |
| 0 | None | Feature not available |
**Comparison Categories:**
Organize features into weighted categories that reflect customer priorities:
| Category | Typical Weight | What to Evaluate |
|----------|---------------|------------------|
| Core Functionality | 25-35% | Primary use case coverage |
| Integration & API | 15-25% | Ecosystem connectivity |
| Security & Compliance | 15-20% | Enterprise readiness |
| Scalability & Performance | 10-20% | Growth capacity |
| Usability & UX | 10-15% | Time to value |
| Support & Services | 5-10% | Vendor partnership quality |
**Weighting Guidelines:**
- Adjust weights based on the specific customer's priorities
- Security-sensitive industries (healthcare, finance) should weight compliance higher
- High-growth companies should weight scalability higher
- Enterprise deals should weight integration and support higher
### 3. Differentiator Identification
A differentiator is a feature or capability where your product scores highest among all compared products. Strong differentiators have these properties:
- **Unique:** Only your product offers this capability
- **Valuable:** Customers care about this capability
- **Defensible:** Not easily replicated by competitors
- **Demonstrable:** Can be shown in a demo or POC
**Differentiator Categories:**
| Type | Description | Example |
|------|-------------|---------|
| Feature Differentiator | Unique product capability | Native ML-powered anomaly detection |
| Architecture Differentiator | Fundamental design advantage | Multi-tenant with data isolation |
| Ecosystem Differentiator | Partner or integration advantage | 200+ native integrations |
| Service Differentiator | Support or engagement model | Dedicated SE throughout contract |
| Economic Differentiator | Pricing or TCO advantage | Usage-based pricing with no minimums |
### 4. Vulnerability Assessment
Vulnerabilities are features where competitors score higher than your product. Address vulnerabilities proactively:
**Vulnerability Response Strategies:**
1. **Acknowledge and redirect:** Confirm the gap, then pivot to your strength areas
2. **Reframe the requirement:** Show why the customer's real need is better met differently
3. **Demonstrate workaround:** Show how existing capabilities address the underlying need
4. **Commit to roadmap:** Provide a credible timeline for native support
5. **Partner solution:** Identify an integration partner that fills the gap
## Objection Handling
### Common Technical Objections
#### "Your product lacks [Feature X]"
**Response Framework:**
1. Acknowledge: "You're right that [Feature X] is not a standalone feature today."
2. Explore: "Help me understand the specific use case you need [Feature X] for."
3. Redirect: "Our approach to solving that is [alternative], which actually provides [benefit]."
4. Evidence: "Customer [reference] had the same concern and found [outcome]."
#### "Competitor [Y] has better [Capability]"
**Response Framework:**
1. Acknowledge: "I understand [Competitor Y] has invested in [Capability]."
2. Qualify: "Can you share what specific aspects of [Capability] are most important?"
3. Differentiate: "While they focus on [approach], we take a different approach with [our method] because [reason]."
4. Quantify: "The practical difference in real-world usage is [metric/evidence]."
#### "Your product is too expensive"
**Response Framework:**
1. Acknowledge: "I appreciate you sharing that concern."
2. Reframe: "Let's look at total cost of ownership rather than license cost alone."
3. Quantify: "When you factor in [implementation, training, maintenance, time-to-value], the TCO comparison shows..."
4. Value: "Based on our analysis, the ROI timeline is [X months], delivering [Y value]."
#### "We're concerned about vendor lock-in"
**Response Framework:**
1. Acknowledge: "That's a smart concern for any technology investment."
2. Evidence: "Our architecture uses [open standards, APIs, data portability features]."
3. Demonstrate: "Here's how data export and migration work [show the feature]."
4. Reference: "We can connect you with customers who evaluated this exact concern."
### Objection Handling Principles
1. **Never disparage competitors.** Focus on your strengths, not their weaknesses.
2. **Ask questions first.** Understand the real concern behind the objection.
3. **Use evidence.** Reference customers, benchmarks, and demonstrations.
4. **Be honest about gaps.** Credibility is your most valuable asset.
5. **Redirect to value.** Connect every response back to business outcomes.
## Win/Loss Analysis
### Post-Decision Review Process
**Timing:** Conduct within 2 weeks of the decision for accurate recall.
**Interview Questions (for wins):**
1. What was the deciding factor in choosing us?
2. Which features or capabilities were most compelling?
3. How did our demo/POC compare to alternatives?
4. What concerns did you have that were resolved during the process?
5. What could we have done better in the evaluation process?
**Interview Questions (for losses):**
1. What was the primary reason for choosing the competitor?
2. Were there specific requirements we did not meet?
3. How did our demo/POC compare to the winning vendor?
4. What would have changed your decision?
5. Would you consider us for future evaluations?
### Win/Loss Data Tracking
| Data Point | Purpose |
|-----------|---------|
| Deal size | Pattern analysis by segment |
| Industry | Vertical-specific insights |
| Competitor | Head-to-head record |
| Decision factors | Feature priority validation |
| Sales cycle length | Process efficiency |
| Stakeholder roles | Engagement strategy |
| Technical requirements | Capability gap tracking |
| POC outcome | POC process improvement |
### Analysis Dimensions
1. **By Competitor:** Win rate per competitor, common objections, feature gaps
2. **By Segment:** Enterprise vs mid-market vs SMB patterns
3. **By Industry:** Vertical-specific win factors
4. **By Deal Size:** Large vs small deal dynamics
5. **By Feature Category:** Which capabilities drive wins vs losses
## Battlecard Creation
### Battlecard Structure
**Page 1: Quick Reference**
- Competitor overview (company size, funding, market position)
- Key strengths (top 3)
- Key weaknesses (top 3)
- Ideal customer profile for the competitor
- Our win rate against this competitor
**Page 2: Feature Comparison**
- Category-by-category comparison (summary view)
- Top differentiators (features where we lead)
- Top vulnerabilities (features where they lead)
- Parity features (features at same level)
**Page 3: Talk Track**
- Opening positioning statement
- Discovery questions that expose competitor weaknesses
- Objection responses for their key strengths
- Proof points (customer references, benchmarks, case studies)
- Trap-setting questions for demos and POCs
**Page 4: Win Strategies**
- Recommended evaluation criteria that favor our strengths
- Demo scenarios that highlight our differentiators
- POC success criteria that align with our capabilities
- Pricing and packaging positioning
- Stakeholder engagement strategy
### Battlecard Maintenance
- **Monthly review:** Update feature scores based on new releases
- **Quarterly refresh:** Incorporate win/loss analysis findings
- **Trigger-based update:** Major competitor release, pricing change, or acquisition
## Competitive Positioning During Evaluations
### Evaluation Stage Tactics
| Stage | Tactic |
|-------|--------|
| Discovery | Ask questions that expose competitor weaknesses |
| Demo | Lead with differentiators, show end-to-end workflows |
| POC | Define success criteria aligned with your strengths |
| Proposal | Quantify TCO advantage, emphasize implementation risk |
| Negotiation | Leverage competitive urgency, offer migration assistance |
### Influencing Evaluation Criteria
The sales engineer's most impactful opportunity is shaping the evaluation criteria before the formal process begins:
1. **Map criteria to strengths:** Propose evaluation categories where you excel
2. **Weight appropriately:** Ensure critical categories (where you lead) carry higher weight
3. **Define metrics:** Specific, measurable criteria favor the more capable product
4. **Include non-obvious criteria:** Total cost of ownership, time-to-value, ecosystem breadth
---
**Last Updated:** February 2026
FILE:references/poc-best-practices.md
# Proof of Concept (POC) Best Practices
A comprehensive guide for Sales Engineers planning, executing, and evaluating proof-of-concept engagements.
## POC Planning Methodology
### 1. Pre-POC Qualification
Not every deal warrants a POC. Qualify before committing resources:
**POC-Worthy Indicators:**
- Deal value justifies 80-200+ hours of SE and engineering time
- Customer has an identified champion who will actively participate
- Clear decision timeline with POC as a defined evaluation step
- Budget is allocated or allocation process is underway
- Technical stakeholders are available for the evaluation period
**POC Red Flags:**
- "Free trial" request with no commitment to evaluate
- No identified decision-maker or budget owner
- Competitor has already been selected; POC is for validation only
- Customer expects production-grade environment for extended period
- No defined success criteria or evaluation framework
### 2. Scope Definition
The most critical success factor is a well-defined scope. An uncontrolled scope leads to extended timelines, unmet expectations, and lost deals.
**Scope Elements:**
- **Use cases:** 3-5 specific scenarios to validate (not "everything")
- **Integrations:** Which systems must connect during the POC
- **Data:** What data will be used (sample, synthetic, production subset)
- **Users:** Who will access the POC environment and in what roles
- **Duration:** Fixed timeline with clear milestones
- **Success criteria:** Measurable, objective criteria for each use case
**Scope Control Tactics:**
- Document scope in writing with customer sign-off
- Define what is explicitly out of scope
- Create a change request process for scope additions
- Set a maximum number of use cases per complexity tier
### 3. Timeline Planning
**Standard 5-Week Framework:**
| Week | Phase | Focus | Key Activities |
|------|-------|-------|---------------|
| 1 | Setup | Foundation | Environment, data, access, kickoff |
| 2-3 | Core Testing | Validation | Primary use cases, integrations, workflows |
| 4 | Advanced Testing | Edge cases | Performance, security, scale, administration |
| 5 | Evaluation | Decision | Scorecard, review, recommendation |
**Timeline Adjustments by Complexity:**
| Complexity | Duration | Use Cases | Integrations |
|-----------|----------|-----------|-------------|
| Low | 3 weeks | 2-3 | 0-1 |
| Medium | 5 weeks | 3-5 | 2-3 |
| High | 6-8 weeks | 5-8 | 4+ |
**Timeline Rules:**
- Never exceed 8 weeks. Longer POCs lose momentum and stakeholder attention.
- Front-load the most impressive capabilities to build early momentum.
- Schedule stakeholder checkpoints at the end of each phase.
- Build 20% buffer into each phase for unexpected issues.
### 4. Resource Planning
**SE Allocation:**
| Activity | Hours/Week (Medium Complexity) |
|----------|-------------------------------|
| Environment setup and configuration | 15-20 (Week 1 only) |
| Use case execution and testing | 20-25 |
| Stakeholder communication | 3-5 |
| Documentation and reporting | 3-5 |
| Issue resolution | 5-8 |
**Engineering Support:**
- Allocate dedicated engineering support for complex integrations
- Establish an escalation path for blocking issues
- Pre-schedule engineering availability during Core Testing phase
- Request customer IT support for integration access and credentials
**Customer Resources:**
- Technical sponsor for daily communication
- Business stakeholders for use case validation
- IT/Security for environment access and compliance review
- End users for usability feedback (if applicable)
## Success Criteria Definition
### Writing Effective Success Criteria
Each criterion must be:
- **Specific:** Clearly defined with no ambiguity
- **Measurable:** Quantifiable metric or clear pass/fail
- **Agreed:** Documented and signed off by both parties
- **Relevant:** Tied to a business outcome or technical requirement
- **Time-bound:** Evaluated within the POC timeline
### Success Criteria Categories
**Functionality Criteria:**
- "System processes [X] transactions per hour without errors"
- "Workflow automation reduces manual steps from [Y] to [Z]"
- "Report generation completes within [N] seconds for [M] records"
- "All [X] defined use cases completed successfully"
**Performance Criteria:**
- "API response time <200ms at p95 under [N] concurrent users"
- "Batch processing completes [X] records in under [Y] minutes"
- "System maintains performance with [N]x expected data volume"
**Integration Criteria:**
- "Bidirectional sync with [System X] operates within [Y] minute latency"
- "SSO integration with [IdP] supports all required authentication flows"
- "Data import from [Source] completes with <1% error rate"
**Usability Criteria:**
- "New users complete [task] within [N] minutes without assistance"
- "Admin configuration for [scenario] requires fewer than [N] steps"
- "Stakeholder satisfaction rating >= 4.0/5.0"
### Anti-Patterns in Success Criteria
- **Too vague:** "System performs well" (what is "well"?)
- **Too many:** More than 15 criteria dilutes focus and extends timeline
- **Unmeasurable:** "Users like the interface" (how do you measure "like"?)
- **Biased toward feature count:** "Must have Feature X" instead of "Must solve Problem Y"
- **Moving target:** Criteria that change mid-POC without formal agreement
## Stakeholder Management
### Stakeholder Map
| Role | Priority | Engagement Strategy |
|------|----------|-------------------|
| Decision Maker | High | Executive briefings, ROI summaries |
| Champion | Critical | Daily communication, progress updates |
| Technical Evaluator | High | Hands-on access, deep-dive sessions |
| End User | Medium | Usability testing, feedback sessions |
| IT/Security | High | Compliance reviews, architecture sessions |
| Procurement | Low-Medium | TCO documentation, reference connections |
### Engagement Cadence
- **Daily:** Champion check-in (10 min, Slack/email)
- **Weekly:** Progress report to all stakeholders (written summary)
- **Phase transitions:** Formal review meeting with demo of progress
- **Final:** Executive presentation with scorecard results and recommendation
### Managing Stakeholder Expectations
1. **Set clear boundaries:** Define what will and will not be demonstrated
2. **Communicate early and often:** No surprises; surface issues immediately
3. **Document everything:** Meeting notes, decisions, change requests
4. **Celebrate wins:** Highlight successful milestones to maintain momentum
5. **Address concerns immediately:** Delays in resolution erode confidence
## Evaluation Frameworks
### Weighted Scorecard Model
The evaluation scorecard provides an objective, comparable assessment:
| Category | Weight | Score (1-5) | Weighted Score |
|----------|--------|-------------|----------------|
| Functionality | 30% | | |
| Performance | 20% | | |
| Integration | 20% | | |
| Usability | 15% | | |
| Support | 15% | | |
| **Total** | **100%** | | |
**Scoring Scale:**
- 5: Exceeds requirements - superior capability demonstrated
- 4: Meets requirements - full capability with minor enhancements possible
- 3: Partially meets - acceptable but notable gaps remain
- 2: Below expectations - significant gaps that impact value
- 1: Does not meet - critical failure for this category
**Decision Thresholds:**
- Weighted average >= 4.0: **Strong Pass** - proceed to procurement
- Weighted average 3.5-3.9: **Pass** - proceed with noted conditions
- Weighted average 3.0-3.4: **Conditional** - requires further evaluation or negotiation
- Weighted average < 3.0: **Fail** - does not meet requirements
### Go/No-Go Decision Framework
The go/no-go decision should be based on multiple factors, not just the scorecard:
**Go Indicators:**
- Scorecard score >= 3.5
- All must-have success criteria met
- Champion and decision-maker both express positive sentiment
- No unresolved critical technical blockers
- Clear implementation path identified
**No-Go Indicators:**
- Scorecard score < 3.0
- Critical success criteria failed without clear resolution
- Decision-maker expresses significant concerns
- Multiple unresolved technical blockers
- Competitive alternative clearly preferred by evaluators
**Conditional Go Indicators:**
- Scorecard score 3.0-3.5 with clear path to improvement
- 1-2 minor success criteria not met but with workarounds
- Mixed stakeholder sentiment that can be addressed
- Blockers identified but resolution path confirmed with engineering
## Common POC Failure Modes
### 1. Scope Creep
**Symptom:** Customer continuously adds requirements during the POC.
**Prevention:** Written scope agreement with change request process.
**Recovery:** Renegotiate timeline or defer additions to Phase 2.
### 2. Champion Absence
**Symptom:** Champion becomes unavailable or disengaged mid-POC.
**Prevention:** Identify a backup champion. Schedule regular touchpoints.
**Recovery:** Escalate to decision-maker. Demonstrate value already achieved.
### 3. Data Issues
**Symptom:** Customer data is unavailable, poor quality, or incompatible.
**Prevention:** Request sample data before kickoff. Prepare synthetic data.
**Recovery:** Use synthetic data for core testing. Document data requirements for implementation.
### 4. Environment Problems
**Symptom:** POC environment is unstable, slow, or inaccessible.
**Prevention:** Use a dedicated, pre-configured environment. Test before kickoff.
**Recovery:** Have a backup environment. Communicate honestly about delays.
### 5. Moving Goalposts
**Symptom:** Evaluation criteria change mid-POC, often influenced by competitor demos.
**Prevention:** Get written sign-off on criteria before starting. Reference agreement when changes arise.
**Recovery:** Agree to evaluate new criteria as addendum, not replacement. Highlight what has already been validated.
### 6. Extended Timeline
**Symptom:** POC drags beyond planned duration without clear progress.
**Prevention:** Set hard deadlines in the agreement. Schedule decision meetings in advance.
**Recovery:** Force a checkpoint. Present results to date and ask for a go/no-go with current evidence.
### 7. Technical Blockers
**Symptom:** Unexpected technical issues prevent completion of key use cases.
**Prevention:** Conduct technical discovery before committing to POC. Have engineering on standby.
**Recovery:** Escalate immediately. Provide transparent status updates. Offer alternative approaches.
## POC Documentation
### Required Artifacts
| Document | When | Owner |
|----------|------|-------|
| Scope agreement | Pre-POC | SE + Customer |
| Environment setup guide | Week 1 | SE |
| Progress reports | Weekly | SE |
| Phase review presentations | Phase transitions | SE |
| Issue log | Ongoing | SE |
| Final evaluation report | Week 5 | SE + Customer |
| Lessons learned | Post-POC | SE |
### Final Report Template
1. **Executive Summary** - POC objectives, approach, and outcome
2. **Scope and Success Criteria** - What was tested and how
3. **Results Summary** - Success criteria outcomes with evidence
4. **Evaluation Scorecard** - Weighted scores across all categories
5. **Issues and Resolutions** - Problems encountered and how they were addressed
6. **Recommendation** - Go/No-Go with rationale
7. **Implementation Considerations** - Next steps, timeline, and resource needs
---
**Last Updated:** February 2026
FILE:references/rfp-response-guide.md
# RFP/RFI Response Guide
A comprehensive reference for Sales Engineers responding to Requests for Proposal (RFP) and Requests for Information (RFI).
## RFP Response Best Practices
### 1. Pre-Response Assessment
Before investing time in a response, conduct a thorough bid/no-bid assessment:
**Bid Criteria Checklist:**
- Do we have a pre-existing relationship with the customer?
- Is there an identified champion or sponsor?
- Do our capabilities align with >70% of requirements?
- Is the deal size justified against the response effort?
- Do we understand the competitive landscape?
- Is the timeline realistic for our solution?
**Red Flags for No-Bid:**
- No prior customer engagement (blind RFP)
- Requirement language mirrors a competitor's product
- Timeline is unrealistically short
- Must-have requirements fall outside our platform
- Budget is undefined or misaligned with our pricing
### 2. Response Organization
**Executive Summary (1-2 pages):**
- Lead with business outcomes, not features
- Reference the customer's specific challenges
- Quantify value proposition with relevant metrics
- State confidence level and key differentiators
**Solution Overview:**
- Map directly to the customer's stated requirements
- Use the customer's language and terminology
- Include architecture diagrams for technical sections
- Address integration with existing systems
**Compliance Matrix:**
- Mirror the RFP's requirement numbering exactly
- Use consistent coverage categories: Full, Partial, Planned, Gap
- Provide clear explanations for each response
- Include roadmap dates for "Planned" items
### 3. Coverage Classification
| Status | Score | Definition | Response Approach |
|--------|-------|------------|-------------------|
| Full | 100% | Current product fully meets requirement | Describe capability with evidence |
| Partial | 50% | Met with configuration or workaround | Explain approach and any limitations |
| Planned | 25% | On product roadmap | Provide timeline and interim solution |
| Gap | 0% | Not currently supported | Acknowledge gap and propose alternatives |
### 4. Priority-Weighted Scoring
Not all requirements are equal. Weight them by business impact:
- **Must-Have (3x weight):** Core requirements that are deal-breakers. Gaps here typically result in disqualification.
- **Should-Have (2x weight):** Important requirements that influence the decision significantly.
- **Nice-to-Have (1x weight):** Desirable but not critical. Often used as tie-breakers.
### 5. Response Writing Tips
**Do:**
- Answer the question directly before elaborating
- Use the customer's terminology, not internal jargon
- Provide specific examples, case studies, and metrics
- Include screenshots or architecture diagrams where relevant
- Cross-reference related answers to avoid redundancy
- Proofread for consistency across sections (multiple authors)
**Avoid:**
- Marketing fluff or vague language ("best-in-class", "world-class")
- Answering a question you were not asked
- Contradictions between sections
- Overselling capabilities you do not have
- Ignoring the question format (tables vs. narrative)
## Bid/No-Bid Decision Framework
### Decision Matrix
| Factor | Weight | Score (1-5) | Weighted |
|--------|--------|-------------|----------|
| Technical fit | 25% | | |
| Relationship strength | 20% | | |
| Competitive position | 20% | | |
| Deal value vs effort | 15% | | |
| Strategic importance | 10% | | |
| Win probability | 10% | | |
| **Total** | **100%** | | |
**Scoring Guide:**
- 5: Strong advantage
- 4: Slight advantage
- 3: Neutral / competitive parity
- 2: Slight disadvantage
- 1: Significant disadvantage
**Decision Thresholds:**
- Score >= 3.5: **Bid** - proceed with full response
- Score 2.5 - 3.4: **Conditional Bid** - proceed with executive approval
- Score < 2.5: **No-Bid** - decline or submit information-only response
### Effort Estimation
Estimate the total effort required and compare against deal value:
| Response Component | Typical Effort (hours) |
|-------------------|----------------------|
| Requirements analysis | 4-8 |
| Technical writing | 16-40 |
| Architecture diagrams | 4-8 |
| Demo preparation | 8-16 |
| Internal review | 4-8 |
| Final formatting | 2-4 |
| **Total** | **38-84 hours** |
**Rule of thumb:** The response effort should not exceed 2% of the deal value.
## Compliance Matrix Structure
### Standard Format
```
| Req ID | Requirement Description | Priority | Compliance | Response | Evidence |
|--------|------------------------|----------|------------|----------|----------|
| R-001 | SSO via SAML 2.0 | Must | Full | Native SAML 2.0 support... | Config guide |
| R-002 | Custom reporting | Should | Partial | Standard reports + API... | API docs |
```
### Section Organization
Organize requirements by category for clarity:
1. **Functional Requirements** - Core features and capabilities
2. **Technical Requirements** - Architecture, APIs, performance
3. **Security & Compliance** - Authentication, encryption, certifications
4. **Integration Requirements** - Third-party systems, data flows
5. **Support & SLA** - Support tiers, response times, uptime
6. **Vendor Qualifications** - Company size, financials, references
## Common Pitfalls
### 1. The Wired RFP
**Symptom:** Requirements language matches a competitor's product feature list.
**Response:** Focus on outcomes over features. Highlight areas of differentiation. Ask clarifying questions that expose broader needs.
### 2. Feature Checklist Syndrome
**Symptom:** RFP is a massive feature checklist with no context about business problems.
**Response:** Group features by business outcome. Add context in your response that demonstrates understanding of the underlying need.
### 3. Scope Creep in Response
**Symptom:** Team keeps adding content that was not requested.
**Response:** Assign a response manager to enforce scope. Answer what was asked, provide references for additional information.
### 4. Inconsistent Messaging
**Symptom:** Multiple authors provide contradictory information.
**Response:** Assign a single editor for final review. Create a response style guide. Use consistent terminology throughout.
### 5. Overcommitting on Gaps
**Symptom:** Marking "Planned" items as "Full" to improve scores.
**Response:** Never misrepresent coverage. Planned items with firm timelines and interim workarounds are better than lies discovered during POC.
## RFP Response Timeline Management
### Typical Response Timeline
| Day | Activity |
|-----|----------|
| Day 1 | Receive RFP, conduct initial review, assign team |
| Day 2-3 | Bid/no-bid decision, questions submission |
| Day 4-7 | Requirements analysis, coverage assessment |
| Day 8-14 | Draft responses, architecture diagrams |
| Day 15-17 | Internal review, quality check |
| Day 18-19 | Final edits, formatting, executive review |
| Day 20 | Submission |
### Time-Saving Strategies
1. **Maintain a response library** - Reusable answers for common requirements
2. **Pre-built architecture diagrams** - Template diagrams for common integration patterns
3. **Standardized compliance language** - Pre-approved language for security and compliance sections
4. **Question templates** - Standard clarifying questions for common ambiguities
---
**Last Updated:** February 2026
FILE:scripts/competitive_matrix_builder.py
#!/usr/bin/env python3
"""Competitive Matrix Builder - Generate feature comparison matrices and positioning analysis.
Builds feature-by-feature comparison matrices, calculates weighted competitive
scores, identifies differentiators and vulnerabilities, and generates win themes.
Usage:
python competitive_matrix_builder.py competitive_data.json
python competitive_matrix_builder.py competitive_data.json --format json
python competitive_matrix_builder.py competitive_data.json --format text
"""
import argparse
import json
import sys
from typing import Any
# Feature scoring levels
FEATURE_SCORES: dict[str, int] = {
"full": 3,
"partial": 2,
"limited": 1,
"none": 0,
}
FEATURE_LABELS: dict[int, str] = {
3: "Full",
2: "Partial",
1: "Limited",
0: "None",
}
def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
"""Safely divide two numbers, returning default if denominator is zero."""
if denominator == 0:
return default
return numerator / denominator
def load_competitive_data(filepath: str) -> dict[str, Any]:
"""Load and validate competitive data from a JSON file.
Args:
filepath: Path to the JSON file containing competitive data.
Returns:
Parsed competitive data dictionary.
Raises:
SystemExit: If the file cannot be read or parsed.
"""
try:
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {filepath}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in {filepath}: {e}", file=sys.stderr)
sys.exit(1)
if "categories" not in data:
print("Error: JSON must contain a 'categories' array.", file=sys.stderr)
sys.exit(1)
if "our_product" not in data:
print("Error: JSON must contain 'our_product' name.", file=sys.stderr)
sys.exit(1)
if "competitors" not in data or not data["competitors"]:
print("Error: JSON must contain a non-empty 'competitors' array.", file=sys.stderr)
sys.exit(1)
return data
def normalize_score(score_value: Any) -> int:
"""Normalize a score value to an integer.
Args:
score_value: Score as string label or integer.
Returns:
Normalized integer score (0-3).
"""
if isinstance(score_value, str):
return FEATURE_SCORES.get(score_value.lower(), 0)
if isinstance(score_value, (int, float)):
return max(0, min(3, int(score_value)))
return 0
def build_comparison_matrix(data: dict[str, Any]) -> dict[str, Any]:
"""Build the feature comparison matrix from input data.
Args:
data: Competitive data with categories, features, and scores.
Returns:
Comparison matrix with per-feature and per-category scores.
"""
our_product = data["our_product"]
competitors = data["competitors"]
all_products = [our_product] + competitors
matrix: list[dict[str, Any]] = []
category_summaries: dict[str, dict[str, Any]] = {}
for category in data["categories"]:
cat_name = category["name"]
cat_weight = category.get("weight", 1.0)
cat_features = category.get("features", [])
cat_scores: dict[str, list[int]] = {p: [] for p in all_products}
for feature in cat_features:
feature_name = feature["name"]
scores: dict[str, int] = {}
for product in all_products:
raw_score = feature.get("scores", {}).get(product, 0)
scores[product] = normalize_score(raw_score)
cat_scores[product].append(scores[product])
# Determine leader for this feature
max_score = max(scores.values())
leaders = [p for p, s in scores.items() if s == max_score]
matrix.append({
"category": cat_name,
"feature": feature_name,
"scores": scores,
"leaders": leaders,
"our_score": scores[our_product],
"max_score": max_score,
"we_lead": our_product in leaders and len(leaders) == 1,
"we_trail": scores[our_product] < max_score,
})
# Category summary
cat_product_scores = {}
for product in all_products:
product_scores = cat_scores[product]
total = sum(product_scores)
max_possible = len(product_scores) * 3
pct = safe_divide(total, max_possible) * 100
cat_product_scores[product] = {
"total_score": total,
"max_possible": max_possible,
"percentage": round(pct, 1),
}
category_summaries[cat_name] = {
"weight": cat_weight,
"feature_count": len(cat_features),
"product_scores": cat_product_scores,
}
return {
"our_product": our_product,
"competitors": competitors,
"all_products": all_products,
"matrix": matrix,
"category_summaries": category_summaries,
}
def compute_competitive_scores(
comparison: dict[str, Any],
) -> dict[str, dict[str, Any]]:
"""Compute weighted competitive scores for each product.
Args:
comparison: Comparison matrix data.
Returns:
Product scores with weighted and unweighted totals.
"""
all_products = comparison["all_products"]
category_summaries = comparison["category_summaries"]
product_scores: dict[str, dict[str, float]] = {
p: {"weighted_total": 0.0, "max_weighted": 0.0, "unweighted_total": 0, "max_unweighted": 0}
for p in all_products
}
for cat_name, cat_data in category_summaries.items():
weight = cat_data["weight"]
for product in all_products:
p_data = cat_data["product_scores"][product]
product_scores[product]["weighted_total"] += p_data["total_score"] * weight
product_scores[product]["max_weighted"] += p_data["max_possible"] * weight
product_scores[product]["unweighted_total"] += p_data["total_score"]
product_scores[product]["max_unweighted"] += p_data["max_possible"]
result = {}
for product in all_products:
ps = product_scores[product]
weighted_pct = safe_divide(ps["weighted_total"], ps["max_weighted"]) * 100
unweighted_pct = safe_divide(ps["unweighted_total"], ps["max_unweighted"]) * 100
result[product] = {
"weighted_score": round(weighted_pct, 1),
"unweighted_score": round(unweighted_pct, 1),
"weighted_total": round(ps["weighted_total"], 2),
"max_weighted": round(ps["max_weighted"], 2),
}
return result
def identify_differentiators(comparison: dict[str, Any]) -> list[dict[str, Any]]:
"""Identify features where our product leads all competitors.
Args:
comparison: Comparison matrix data.
Returns:
List of differentiator features with details.
"""
differentiators = []
for entry in comparison["matrix"]:
if entry["we_lead"] and entry["our_score"] >= 2:
# Calculate gap from nearest competitor
competitor_scores = [
entry["scores"][c] for c in comparison["competitors"]
]
max_competitor = max(competitor_scores) if competitor_scores else 0
gap = entry["our_score"] - max_competitor
differentiators.append({
"feature": entry["feature"],
"category": entry["category"],
"our_score": entry["our_score"],
"our_label": FEATURE_LABELS.get(entry["our_score"], "Unknown"),
"best_competitor_score": max_competitor,
"gap": gap,
})
# Sort by gap size descending
differentiators.sort(key=lambda d: d["gap"], reverse=True)
return differentiators
def identify_vulnerabilities(comparison: dict[str, Any]) -> list[dict[str, Any]]:
"""Identify features where competitors lead our product.
Args:
comparison: Comparison matrix data.
Returns:
List of vulnerability features with details.
"""
vulnerabilities = []
for entry in comparison["matrix"]:
if entry["we_trail"]:
# Find which competitor leads
leader_scores = {
p: entry["scores"][p]
for p in comparison["competitors"]
if entry["scores"][p] == entry["max_score"]
}
gap = entry["max_score"] - entry["our_score"]
vulnerabilities.append({
"feature": entry["feature"],
"category": entry["category"],
"our_score": entry["our_score"],
"our_label": FEATURE_LABELS.get(entry["our_score"], "Unknown"),
"leading_competitors": leader_scores,
"gap": gap,
})
# Sort by gap size descending
vulnerabilities.sort(key=lambda v: v["gap"], reverse=True)
return vulnerabilities
def generate_win_themes(
differentiators: list[dict[str, Any]],
competitive_scores: dict[str, dict[str, Any]],
our_product: str,
) -> list[str]:
"""Generate win themes based on differentiators and competitive position.
Args:
differentiators: List of differentiator features.
competitive_scores: Product competitive scores.
our_product: Our product name.
Returns:
List of win theme strings.
"""
themes = []
# Theme from top differentiators
if differentiators:
top_diff_categories = list({d["category"] for d in differentiators[:5]})
for cat in top_diff_categories[:3]:
cat_diffs = [d for d in differentiators if d["category"] == cat]
feature_names = [d["feature"] for d in cat_diffs[:3]]
themes.append(
f"Superior {cat} capabilities: {', '.join(feature_names)}"
)
# Theme from overall competitive position
our_score = competitive_scores.get(our_product, {}).get("weighted_score", 0)
competitor_scores = [
(p, s["weighted_score"])
for p, s in competitive_scores.items()
if p != our_product
]
if competitor_scores:
best_competitor_name, best_competitor_score = max(
competitor_scores, key=lambda x: x[1]
)
if our_score > best_competitor_score:
themes.append(
f"Overall strongest solution ({our_score:.1f}% vs {best_competitor_name} at {best_competitor_score:.1f}%)"
)
# Theme from breadth of coverage
strong_diffs = [d for d in differentiators if d["gap"] >= 2]
if len(strong_diffs) >= 3:
themes.append(
f"Clear technical leadership across {len(strong_diffs)} key features with significant competitive gaps"
)
if not themes:
themes.append("Competitive parity - emphasize implementation quality, support, and total cost of ownership")
return themes
def analyze_competitive(data: dict[str, Any]) -> dict[str, Any]:
"""Run the complete competitive analysis pipeline.
Args:
data: Parsed competitive data dictionary.
Returns:
Complete analysis results dictionary.
"""
comparison = build_comparison_matrix(data)
competitive_scores = compute_competitive_scores(comparison)
differentiators = identify_differentiators(comparison)
vulnerabilities = identify_vulnerabilities(comparison)
win_themes = generate_win_themes(
differentiators, competitive_scores, comparison["our_product"]
)
return {
"analysis_info": {
"our_product": comparison["our_product"],
"competitors": comparison["competitors"],
"total_features": len(comparison["matrix"]),
"total_categories": len(comparison["category_summaries"]),
},
"competitive_scores": competitive_scores,
"category_breakdown": comparison["category_summaries"],
"comparison_matrix": comparison["matrix"],
"differentiators": differentiators,
"vulnerabilities": vulnerabilities,
"win_themes": win_themes,
}
def format_text(result: dict[str, Any]) -> str:
"""Format analysis results as human-readable text.
Args:
result: Complete analysis results dictionary.
Returns:
Formatted text string.
"""
lines = []
info = result["analysis_info"]
all_products = [info["our_product"]] + info["competitors"]
lines.append("=" * 80)
lines.append("COMPETITIVE MATRIX ANALYSIS")
lines.append("=" * 80)
lines.append(f"Our Product: {info['our_product']}")
lines.append(f"Competitors: {', '.join(info['competitors'])}")
lines.append(f"Features: {info['total_features']}")
lines.append(f"Categories: {info['total_categories']}")
lines.append("")
# Competitive scores
lines.append("-" * 80)
lines.append("COMPETITIVE SCORES")
lines.append("-" * 80)
lines.append(f"{'Product':<25} {'Weighted':>10} {'Unweighted':>12}")
lines.append("-" * 80)
# Sort by weighted score descending
sorted_scores = sorted(
result["competitive_scores"].items(),
key=lambda x: x[1]["weighted_score"],
reverse=True,
)
for product, scores in sorted_scores:
marker = " <-- US" if product == info["our_product"] else ""
lines.append(
f"{product:<25} {scores['weighted_score']:>9.1f}% {scores['unweighted_score']:>11.1f}%{marker}"
)
lines.append("")
# Feature matrix
lines.append("-" * 80)
lines.append("FEATURE COMPARISON MATRIX")
lines.append("-" * 80)
# Build header
product_cols = " ".join(f"{p[:10]:>10}" for p in all_products)
lines.append(f"{'Feature':<30} {product_cols}")
lines.append("-" * 80)
current_category = ""
for entry in result["comparison_matrix"]:
if entry["category"] != current_category:
current_category = entry["category"]
cat_data = result["category_breakdown"].get(current_category, {})
weight = cat_data.get("weight", 1.0)
lines.append(f"\n [{current_category}] (weight: {weight}x)")
score_cols = " ".join(
f"{FEATURE_LABELS.get(entry['scores'].get(p, 0), 'N/A'):>10}"
for p in all_products
)
lead_marker = " *" if entry["we_lead"] else (" !" if entry["we_trail"] else "")
feature_display = entry["feature"][:28]
lines.append(f" {feature_display:<28} {score_cols}{lead_marker}")
lines.append("")
lines.append(" * = We lead | ! = We trail")
lines.append("")
# Differentiators
diffs = result["differentiators"]
if diffs:
lines.append("-" * 80)
lines.append(f"DIFFERENTIATORS ({len(diffs)} features where we lead)")
lines.append("-" * 80)
for d in diffs:
lines.append(
f" + {d['feature']} [{d['category']}] "
f"- Us: {d['our_label']} vs Best Competitor: {FEATURE_LABELS.get(d['best_competitor_score'], 'N/A')} "
f"(gap: +{d['gap']})"
)
lines.append("")
# Vulnerabilities
vulns = result["vulnerabilities"]
if vulns:
lines.append("-" * 80)
lines.append(f"VULNERABILITIES ({len(vulns)} features where competitors lead)")
lines.append("-" * 80)
for v in vulns:
leaders = ", ".join(
f"{p}: {FEATURE_LABELS.get(s, 'N/A')}"
for p, s in v["leading_competitors"].items()
)
lines.append(
f" - {v['feature']} [{v['category']}] "
f"- Us: {v['our_label']} vs {leaders} "
f"(gap: -{v['gap']})"
)
lines.append("")
# Win themes
themes = result["win_themes"]
lines.append("-" * 80)
lines.append("WIN THEMES")
lines.append("-" * 80)
for i, theme in enumerate(themes, 1):
lines.append(f" {i}. {theme}")
lines.append("")
lines.append("=" * 80)
return "\n".join(lines)
def main() -> None:
"""Main entry point for the Competitive Matrix Builder."""
parser = argparse.ArgumentParser(
description="Build competitive feature comparison matrices and positioning analysis.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Feature Scoring:\n"
" Full (3) - Complete feature support\n"
" Partial (2) - Partial or limited support\n"
" Limited (1) - Minimal or basic support\n"
" None (0) - Feature not available\n"
"\n"
"Example:\n"
" python competitive_matrix_builder.py competitive_data.json --format json\n"
),
)
parser.add_argument(
"input_file",
help="Path to JSON file containing competitive data",
)
parser.add_argument(
"--format",
choices=["json", "text"],
default="text",
dest="output_format",
help="Output format: json or text (default: text)",
)
args = parser.parse_args()
data = load_competitive_data(args.input_file)
result = analyze_competitive(data)
if args.output_format == "json":
print(json.dumps(result, indent=2))
else:
print(format_text(result))
if __name__ == "__main__":
main()
FILE:scripts/poc_planner.py
#!/usr/bin/env python3
"""POC Planner - Plan proof-of-concept engagements with timeline, resources, and scorecards.
Generates structured POC plans including phased timelines, resource allocation,
success criteria with measurable metrics, evaluation scorecards, risk identification,
and go/no-go recommendation frameworks.
Usage:
python poc_planner.py poc_data.json
python poc_planner.py poc_data.json --format json
python poc_planner.py poc_data.json --format text
"""
import argparse
import json
import sys
from typing import Any
# Default phase definitions
DEFAULT_PHASES = [
{
"name": "Setup",
"duration_weeks": 1,
"description": "Environment provisioning, data migration, initial configuration",
"activities": [
"Provision POC environment",
"Configure authentication and access",
"Migrate sample data sets",
"Set up monitoring and logging",
"Conduct kickoff meeting with stakeholders",
],
},
{
"name": "Core Testing",
"duration_weeks": 2,
"description": "Primary use case validation and integration testing",
"activities": [
"Execute primary use case scenarios",
"Test core integrations",
"Validate data flow and transformations",
"Conduct mid-point review with stakeholders",
"Document findings and adjust test plan",
],
},
{
"name": "Advanced Testing",
"duration_weeks": 1,
"description": "Edge cases, performance testing, and security validation",
"activities": [
"Execute edge case scenarios",
"Run performance and load tests",
"Validate security controls and compliance",
"Test disaster recovery and failover",
"Test administrative workflows",
],
},
{
"name": "Evaluation",
"duration_weeks": 1,
"description": "Scorecard completion, stakeholder review, and go/no-go decision",
"activities": [
"Complete evaluation scorecard",
"Compile POC results documentation",
"Conduct final stakeholder review",
"Present go/no-go recommendation",
"Gather lessons learned",
],
},
]
# Evaluation categories with default weights
DEFAULT_EVAL_CATEGORIES = {
"Functionality": {
"weight": 0.30,
"criteria": [
"Core feature completeness",
"Use case coverage",
"Customization flexibility",
"Workflow automation",
],
},
"Performance": {
"weight": 0.20,
"criteria": [
"Response time under load",
"Throughput capacity",
"Scalability characteristics",
"Resource utilization",
],
},
"Integration": {
"weight": 0.20,
"criteria": [
"API completeness and documentation",
"Data migration ease",
"Third-party connector availability",
"Authentication/SSO integration",
],
},
"Usability": {
"weight": 0.15,
"criteria": [
"User interface intuitiveness",
"Learning curve assessment",
"Documentation quality",
"Admin console functionality",
],
},
"Support": {
"weight": 0.15,
"criteria": [
"Technical support responsiveness",
"Knowledge base quality",
"Training resources availability",
"Community and ecosystem",
],
},
}
def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
"""Safely divide two numbers, returning default if denominator is zero."""
if denominator == 0:
return default
return numerator / denominator
def load_poc_data(filepath: str) -> dict[str, Any]:
"""Load and validate POC data from a JSON file.
Args:
filepath: Path to the JSON file containing POC data.
Returns:
Parsed POC data dictionary.
Raises:
SystemExit: If the file cannot be read or parsed.
"""
try:
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {filepath}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in {filepath}: {e}", file=sys.stderr)
sys.exit(1)
if "poc_name" not in data:
print("Error: JSON must contain 'poc_name' field.", file=sys.stderr)
sys.exit(1)
return data
def estimate_resources(data: dict[str, Any], phases: list[dict[str, Any]]) -> dict[str, Any]:
"""Estimate resource requirements for the POC.
Args:
data: POC data with scope and requirements.
phases: List of phase definitions.
Returns:
Resource allocation dictionary.
"""
total_weeks = sum(p["duration_weeks"] for p in phases)
complexity = data.get("complexity", "medium").lower()
scope_items = data.get("scope_items", [])
num_integrations = data.get("num_integrations", 0)
# Base SE hours per week by complexity
se_hours_per_week = {"low": 15, "medium": 25, "high": 35}.get(complexity, 25)
# Engineering support hours
eng_base = {"low": 5, "medium": 10, "high": 20}.get(complexity, 10)
eng_integration_hours = num_integrations * 8
# Customer resource hours
customer_hours_per_week = {"low": 5, "medium": 8, "high": 12}.get(complexity, 8)
se_total = se_hours_per_week * total_weeks
eng_total = (eng_base * total_weeks) + eng_integration_hours
customer_total = customer_hours_per_week * total_weeks
# Phase-level breakdown
phase_resources = []
for phase in phases:
weeks = phase["duration_weeks"]
# Setup phase has higher SE and eng effort
se_multiplier = 1.3 if phase["name"] == "Setup" else (
1.0 if phase["name"] in ("Core Testing", "Advanced Testing") else 0.7
)
eng_multiplier = 1.5 if phase["name"] == "Setup" else (
1.0 if phase["name"] == "Core Testing" else (
1.2 if phase["name"] == "Advanced Testing" else 0.5
)
)
phase_resources.append({
"phase": phase["name"],
"duration_weeks": weeks,
"se_hours": round(se_hours_per_week * weeks * se_multiplier),
"engineering_hours": round(eng_base * weeks * eng_multiplier),
"customer_hours": round(customer_hours_per_week * weeks),
})
return {
"total_duration_weeks": total_weeks,
"complexity": complexity,
"totals": {
"se_hours": se_total,
"engineering_hours": eng_total,
"customer_hours": customer_total,
"total_hours": se_total + eng_total + customer_total,
},
"phase_breakdown": phase_resources,
"additional_resources": {
"integration_hours": eng_integration_hours,
"num_integrations": num_integrations,
},
}
def generate_success_criteria(data: dict[str, Any]) -> list[dict[str, Any]]:
"""Generate success criteria based on POC scope and requirements.
Args:
data: POC data with scope and requirements.
Returns:
List of success criteria with metrics.
"""
criteria = []
# Custom criteria from input
custom_criteria = data.get("success_criteria", [])
for cc in custom_criteria:
criteria.append({
"criterion": cc.get("criterion", "Unnamed criterion"),
"metric": cc.get("metric", "Pass/Fail"),
"target": cc.get("target", "Met"),
"category": cc.get("category", "Functionality"),
"priority": cc.get("priority", "must-have"),
})
# Auto-generated criteria based on scope
scope_items = data.get("scope_items", [])
for item in scope_items:
if isinstance(item, str):
criteria.append({
"criterion": f"Validate: {item}",
"metric": "Pass/Fail",
"target": "Pass",
"category": "Functionality",
"priority": "must-have",
})
elif isinstance(item, dict):
criteria.append({
"criterion": item.get("name", "Unnamed scope item"),
"metric": item.get("metric", "Pass/Fail"),
"target": item.get("target", "Pass"),
"category": item.get("category", "Functionality"),
"priority": item.get("priority", "must-have"),
})
# Default criteria if none provided
if not criteria:
criteria = [
{
"criterion": "Core use case validation",
"metric": "Percentage of use cases successfully demonstrated",
"target": ">90%",
"category": "Functionality",
"priority": "must-have",
},
{
"criterion": "Performance under expected load",
"metric": "Response time at target concurrency",
"target": "<2 seconds p95",
"category": "Performance",
"priority": "must-have",
},
{
"criterion": "Integration with existing systems",
"metric": "Number of integrations successfully tested",
"target": "All planned integrations",
"category": "Integration",
"priority": "must-have",
},
{
"criterion": "User acceptance",
"metric": "Stakeholder satisfaction score",
"target": ">4.0/5.0",
"category": "Usability",
"priority": "should-have",
},
]
return criteria
def generate_evaluation_scorecard(data: dict[str, Any]) -> dict[str, Any]:
"""Generate the POC evaluation scorecard template.
Args:
data: POC data.
Returns:
Evaluation scorecard structure.
"""
custom_categories = data.get("evaluation_categories", {})
# Merge custom categories with defaults
categories = {}
for cat_name, cat_data in DEFAULT_EVAL_CATEGORIES.items():
if cat_name in custom_categories:
custom = custom_categories[cat_name]
categories[cat_name] = {
"weight": custom.get("weight", cat_data["weight"]),
"criteria": custom.get("criteria", cat_data["criteria"]),
"score": None,
"notes": "",
}
else:
categories[cat_name] = {
"weight": cat_data["weight"],
"criteria": cat_data["criteria"],
"score": None,
"notes": "",
}
# Normalize weights to sum to 1.0
total_weight = sum(c["weight"] for c in categories.values())
if total_weight > 0 and abs(total_weight - 1.0) > 0.01:
for cat in categories.values():
cat["weight"] = round(safe_divide(cat["weight"], total_weight), 2)
return {
"scoring_scale": {
"5": "Exceeds requirements - superior capability",
"4": "Meets requirements - full capability",
"3": "Partially meets - acceptable with minor gaps",
"2": "Below expectations - significant gaps",
"1": "Does not meet - critical gaps",
},
"categories": categories,
"pass_threshold": 3.5,
"strong_pass_threshold": 4.0,
}
def identify_risks(data: dict[str, Any], resources: dict[str, Any]) -> list[dict[str, Any]]:
"""Identify POC risks and generate mitigation strategies.
Args:
data: POC data.
resources: Resource allocation data.
Returns:
List of risk entries with probability, impact, and mitigation.
"""
risks = []
complexity = data.get("complexity", "medium").lower()
num_integrations = data.get("num_integrations", 0)
total_weeks = resources["total_duration_weeks"]
stakeholders = data.get("stakeholders", [])
# Timeline risk
if total_weeks > 6:
risks.append({
"risk": "Extended timeline may lose stakeholder attention",
"probability": "high",
"impact": "high",
"mitigation": "Schedule weekly progress checkpoints; deliver early wins in week 2",
"category": "Timeline",
})
elif total_weeks >= 4:
risks.append({
"risk": "Timeline may slip due to unforeseen technical issues",
"probability": "medium",
"impact": "medium",
"mitigation": "Build 20% buffer into each phase; identify critical path early",
"category": "Timeline",
})
# Integration risks
if num_integrations > 3:
risks.append({
"risk": "Multiple integrations increase complexity and failure points",
"probability": "high",
"impact": "high",
"mitigation": "Prioritize integrations by business value; test incrementally; have fallback demo data",
"category": "Technical",
})
elif num_integrations > 0:
risks.append({
"risk": "Integration dependencies may cause delays",
"probability": "medium",
"impact": "medium",
"mitigation": "Engage customer IT early; confirm API access and credentials in setup phase",
"category": "Technical",
})
# Data risks
risks.append({
"risk": "Customer data quality or availability issues",
"probability": "medium",
"impact": "high",
"mitigation": "Request sample data early; prepare synthetic data as fallback; validate data format in setup",
"category": "Data",
})
# Stakeholder risks
if len(stakeholders) > 5:
risks.append({
"risk": "Too many stakeholders may slow decision-making",
"probability": "medium",
"impact": "medium",
"mitigation": "Identify decision-maker and champion; schedule focused reviews per stakeholder group",
"category": "Stakeholder",
})
if not stakeholders:
risks.append({
"risk": "Undefined stakeholder map may lead to misaligned evaluation",
"probability": "high",
"impact": "high",
"mitigation": "Confirm stakeholder list, roles, and evaluation criteria before setup phase",
"category": "Stakeholder",
})
# Resource risks
if complexity == "high":
risks.append({
"risk": "High complexity may require additional engineering resources",
"probability": "medium",
"impact": "high",
"mitigation": "Secure engineering commitment upfront; identify escalation path for blockers",
"category": "Resource",
})
# Competitive risk
risks.append({
"risk": "Competitor POC running in parallel may shift evaluation criteria",
"probability": "medium",
"impact": "medium",
"mitigation": "Stay close to champion; align success criteria early; differentiate on unique strengths",
"category": "Competitive",
})
return risks
def generate_go_no_go_framework(data: dict[str, Any]) -> dict[str, Any]:
"""Generate the go/no-go decision framework.
Args:
data: POC data.
Returns:
Go/no-go framework with criteria and thresholds.
"""
return {
"decision_criteria": [
{
"criterion": "Overall scorecard score",
"go_threshold": ">=3.5 weighted average",
"no_go_threshold": "<3.0 weighted average",
"conditional_range": "3.0 - 3.5",
},
{
"criterion": "Must-have success criteria met",
"go_threshold": "100% of must-have criteria pass",
"no_go_threshold": "<80% of must-have criteria pass",
"conditional_range": "80-99% with mitigation plan",
},
{
"criterion": "Stakeholder satisfaction",
"go_threshold": "Champion and decision-maker both positive",
"no_go_threshold": "Decision-maker negative",
"conditional_range": "Mixed signals - needs follow-up",
},
{
"criterion": "Technical blockers",
"go_threshold": "No unresolved critical blockers",
"no_go_threshold": ">2 unresolved critical blockers",
"conditional_range": "1-2 blockers with clear resolution path",
},
],
"recommendation_logic": {
"GO": "All criteria meet go thresholds, or majority go with no no-go triggers",
"CONDITIONAL_GO": "Some criteria in conditional range, but no no-go triggers and clear resolution plan",
"NO_GO": "Any criterion triggers no-go threshold without clear mitigation",
},
}
def plan_poc(data: dict[str, Any]) -> dict[str, Any]:
"""Run the complete POC planning pipeline.
Args:
data: Parsed POC data dictionary.
Returns:
Complete POC plan dictionary.
"""
poc_info = {
"poc_name": data.get("poc_name", "Unnamed POC"),
"customer": data.get("customer", "Unknown Customer"),
"opportunity_value": data.get("opportunity_value", "Not specified"),
"complexity": data.get("complexity", "medium"),
"start_date": data.get("start_date", "TBD"),
"champion": data.get("champion", "Not identified"),
"decision_maker": data.get("decision_maker", "Not identified"),
}
# Use custom phases if provided, otherwise defaults
phases = data.get("phases", DEFAULT_PHASES)
# Resource estimation
resources = estimate_resources(data, phases)
# Success criteria
success_criteria = generate_success_criteria(data)
# Evaluation scorecard
scorecard = generate_evaluation_scorecard(data)
# Risk identification
risks = identify_risks(data, resources)
# Go/No-Go framework
go_no_go = generate_go_no_go_framework(data)
# Timeline with phase details
timeline = []
current_week = 1
for phase in phases:
end_week = current_week + phase["duration_weeks"] - 1
timeline.append({
"phase": phase["name"],
"start_week": current_week,
"end_week": end_week,
"duration_weeks": phase["duration_weeks"],
"description": phase["description"],
"activities": phase["activities"],
})
current_week = end_week + 1
# Stakeholder plan
stakeholders = data.get("stakeholders", [])
stakeholder_plan = []
for s in stakeholders:
if isinstance(s, str):
stakeholder_plan.append({
"name": s,
"role": "Evaluator",
"engagement": "Weekly updates, phase reviews",
})
elif isinstance(s, dict):
stakeholder_plan.append({
"name": s.get("name", "Unknown"),
"role": s.get("role", "Evaluator"),
"engagement": s.get("engagement", "Weekly updates, phase reviews"),
})
return {
"poc_info": poc_info,
"timeline": timeline,
"resource_allocation": resources,
"success_criteria": success_criteria,
"evaluation_scorecard": scorecard,
"risk_register": risks,
"go_no_go_framework": go_no_go,
"stakeholder_plan": stakeholder_plan,
}
def format_text(result: dict[str, Any]) -> str:
"""Format POC plan as human-readable text.
Args:
result: Complete POC plan dictionary.
Returns:
Formatted text string.
"""
lines = []
info = result["poc_info"]
lines.append("=" * 70)
lines.append("PROOF OF CONCEPT PLAN")
lines.append("=" * 70)
lines.append(f"POC Name: {info['poc_name']}")
lines.append(f"Customer: {info['customer']}")
lines.append(f"Opportunity Value: {info['opportunity_value']}")
lines.append(f"Complexity: {info['complexity'].upper()}")
lines.append(f"Start Date: {info['start_date']}")
lines.append(f"Champion: {info['champion']}")
lines.append(f"Decision Maker: {info['decision_maker']}")
lines.append("")
# Timeline
lines.append("-" * 70)
lines.append("TIMELINE")
lines.append("-" * 70)
for phase in result["timeline"]:
week_range = (
f"Week {phase['start_week']}"
if phase["start_week"] == phase["end_week"]
else f"Weeks {phase['start_week']}-{phase['end_week']}"
)
lines.append(f"\n Phase: {phase['phase']} ({week_range})")
lines.append(f" {phase['description']}")
lines.append(" Activities:")
for activity in phase["activities"]:
lines.append(f" - {activity}")
lines.append("")
# Resource allocation
res = result["resource_allocation"]
lines.append("-" * 70)
lines.append("RESOURCE ALLOCATION")
lines.append("-" * 70)
lines.append(f"Total Duration: {res['total_duration_weeks']} weeks")
lines.append(f"Complexity: {res['complexity'].upper()}")
lines.append("")
lines.append(" Totals:")
lines.append(f" SE Hours: {res['totals']['se_hours']}")
lines.append(f" Engineering Hours: {res['totals']['engineering_hours']}")
lines.append(f" Customer Hours: {res['totals']['customer_hours']}")
lines.append(f" Total Hours: {res['totals']['total_hours']}")
lines.append("")
lines.append(" Phase Breakdown:")
lines.append(f" {'Phase':<20} {'Weeks':>5} {'SE':>6} {'Eng':>6} {'Cust':>6}")
lines.append(" " + "-" * 45)
for pr in res["phase_breakdown"]:
lines.append(
f" {pr['phase']:<20} {pr['duration_weeks']:>5} "
f"{pr['se_hours']:>5}h {pr['engineering_hours']:>5}h {pr['customer_hours']:>5}h"
)
lines.append("")
# Success criteria
criteria = result["success_criteria"]
lines.append("-" * 70)
lines.append("SUCCESS CRITERIA")
lines.append("-" * 70)
for i, sc in enumerate(criteria, 1):
priority_marker = "[MUST]" if sc["priority"] == "must-have" else (
"[SHOULD]" if sc["priority"] == "should-have" else "[NICE]"
)
lines.append(f" {i}. {priority_marker} {sc['criterion']}")
lines.append(f" Metric: {sc['metric']}")
lines.append(f" Target: {sc['target']}")
lines.append(f" Category: {sc['category']}")
lines.append("")
# Evaluation scorecard
scorecard = result["evaluation_scorecard"]
lines.append("-" * 70)
lines.append("EVALUATION SCORECARD")
lines.append("-" * 70)
lines.append(f" Pass Threshold: {scorecard['pass_threshold']}/5.0")
lines.append(f" Strong Pass Threshold: {scorecard['strong_pass_threshold']}/5.0")
lines.append("")
lines.append(" Scoring Scale:")
for score, desc in scorecard["scoring_scale"].items():
lines.append(f" {score} = {desc}")
lines.append("")
lines.append(" Categories:")
for cat_name, cat_data in scorecard["categories"].items():
lines.append(f"\n {cat_name} (weight: {cat_data['weight']:.0%})")
for criterion in cat_data["criteria"]:
lines.append(f" [ ] {criterion}")
lines.append("")
# Risk register
risks = result["risk_register"]
lines.append("-" * 70)
lines.append("RISK REGISTER")
lines.append("-" * 70)
for risk in risks:
lines.append(f" [{risk['impact'].upper()}] {risk['risk']}")
lines.append(f" Probability: {risk['probability']} | Impact: {risk['impact']}")
lines.append(f" Category: {risk['category']}")
lines.append(f" Mitigation: {risk['mitigation']}")
lines.append("")
# Go/No-Go framework
framework = result["go_no_go_framework"]
lines.append("-" * 70)
lines.append("GO / NO-GO DECISION FRAMEWORK")
lines.append("-" * 70)
for dc in framework["decision_criteria"]:
lines.append(f" {dc['criterion']}:")
lines.append(f" GO: {dc['go_threshold']}")
lines.append(f" CONDITIONAL: {dc['conditional_range']}")
lines.append(f" NO-GO: {dc['no_go_threshold']}")
lines.append("")
lines.append(" Recommendation Logic:")
for decision, logic in framework["recommendation_logic"].items():
lines.append(f" {decision}: {logic}")
lines.append("")
# Stakeholder plan
stakeholders = result["stakeholder_plan"]
if stakeholders:
lines.append("-" * 70)
lines.append("STAKEHOLDER PLAN")
lines.append("-" * 70)
for s in stakeholders:
lines.append(f" {s['name']} ({s['role']})")
lines.append(f" Engagement: {s['engagement']}")
lines.append("")
lines.append("=" * 70)
return "\n".join(lines)
def main() -> None:
"""Main entry point for the POC Planner."""
parser = argparse.ArgumentParser(
description="Plan proof-of-concept engagements with timeline, resources, and evaluation scorecards.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Default Phases:\n"
" Week 1: Setup - Environment provisioning, configuration\n"
" Weeks 2-3: Core Testing - Primary use cases, integrations\n"
" Week 4: Advanced Testing - Edge cases, performance, security\n"
" Week 5: Evaluation - Scorecard, stakeholder review, go/no-go\n"
"\n"
"Example:\n"
" python poc_planner.py poc_data.json --format json\n"
),
)
parser.add_argument(
"input_file",
help="Path to JSON file containing POC scope and requirements",
)
parser.add_argument(
"--format",
choices=["json", "text"],
default="text",
dest="output_format",
help="Output format: json or text (default: text)",
)
args = parser.parse_args()
data = load_poc_data(args.input_file)
result = plan_poc(data)
if args.output_format == "json":
print(json.dumps(result, indent=2))
else:
print(format_text(result))
if __name__ == "__main__":
main()
FILE:scripts/rfp_response_analyzer.py
#!/usr/bin/env python3
"""RFP/RFI Response Analyzer - Score coverage, identify gaps, and recommend bid/no-bid.
Parses RFP/RFI requirements and scores coverage using Full/Partial/Planned/Gap
categories. Generates weighted coverage scores, gap analysis with mitigation
strategies, effort estimation, and bid/no-bid recommendations.
Usage:
python rfp_response_analyzer.py rfp_data.json
python rfp_response_analyzer.py rfp_data.json --format json
python rfp_response_analyzer.py rfp_data.json --format text
"""
import argparse
import json
import sys
from typing import Any
# Coverage status to score mapping
COVERAGE_SCORES: dict[str, float] = {
"full": 1.0,
"partial": 0.5,
"planned": 0.25,
"gap": 0.0,
}
# Priority to weight mapping
PRIORITY_WEIGHTS: dict[str, float] = {
"must-have": 3.0,
"should-have": 2.0,
"nice-to-have": 1.0,
}
# Bid thresholds
BID_THRESHOLD = 0.70
CONDITIONAL_THRESHOLD = 0.50
MAX_MUST_HAVE_GAPS_FOR_BID = 3
def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
"""Safely divide two numbers, returning default if denominator is zero."""
if denominator == 0:
return default
return numerator / denominator
def load_rfp_data(filepath: str) -> dict[str, Any]:
"""Load and validate RFP data from a JSON file.
Args:
filepath: Path to the JSON file containing RFP data.
Returns:
Parsed RFP data dictionary.
Raises:
SystemExit: If the file cannot be read or parsed.
"""
try:
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {filepath}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in {filepath}: {e}", file=sys.stderr)
sys.exit(1)
if "requirements" not in data:
print("Error: JSON must contain a 'requirements' array.", file=sys.stderr)
sys.exit(1)
return data
def analyze_requirement(req: dict[str, Any]) -> dict[str, Any]:
"""Analyze a single requirement and compute its score.
Args:
req: Requirement dictionary with category, priority, coverage_status, etc.
Returns:
Enriched requirement with computed score and weight.
"""
coverage_status = req.get("coverage_status", "gap").lower()
priority = req.get("priority", "nice-to-have").lower()
coverage_score = COVERAGE_SCORES.get(coverage_status, 0.0)
weight = PRIORITY_WEIGHTS.get(priority, 1.0)
weighted_score = coverage_score * weight
max_weighted = weight
effort_hours = req.get("effort_hours", 0)
result = {
"id": req.get("id", "unknown"),
"requirement": req.get("requirement", "Unnamed requirement"),
"category": req.get("category", "Uncategorized"),
"priority": priority,
"coverage_status": coverage_status,
"coverage_score": coverage_score,
"weight": weight,
"weighted_score": weighted_score,
"max_weighted": max_weighted,
"effort_hours": effort_hours,
"notes": req.get("notes", ""),
"mitigation": req.get("mitigation", ""),
}
return result
def generate_gap_analysis(analyzed_reqs: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Generate gap analysis for requirements not fully covered.
Args:
analyzed_reqs: List of analyzed requirement dictionaries.
Returns:
List of gap entries with mitigation strategies.
"""
gaps = []
for req in analyzed_reqs:
if req["coverage_status"] in ("gap", "partial", "planned"):
severity = "critical" if req["priority"] == "must-have" else (
"high" if req["priority"] == "should-have" else "low"
)
mitigation = req["mitigation"]
if not mitigation:
if req["coverage_status"] == "partial":
mitigation = "Enhance existing capability to achieve full coverage"
elif req["coverage_status"] == "planned":
mitigation = "Communicate roadmap timeline and interim workaround"
else:
mitigation = "Evaluate build vs. partner vs. no-bid for this requirement"
gaps.append({
"id": req["id"],
"requirement": req["requirement"],
"category": req["category"],
"priority": req["priority"],
"coverage_status": req["coverage_status"],
"severity": severity,
"effort_hours": req["effort_hours"],
"mitigation": mitigation,
})
# Sort by severity: critical > high > low
severity_order = {"critical": 0, "high": 1, "low": 2}
gaps.sort(key=lambda g: severity_order.get(g["severity"], 3))
return gaps
def compute_category_scores(analyzed_reqs: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
"""Compute coverage scores grouped by requirement category.
Args:
analyzed_reqs: List of analyzed requirement dictionaries.
Returns:
Dictionary of category names to score summaries.
"""
categories: dict[str, dict[str, float]] = {}
for req in analyzed_reqs:
cat = req["category"]
if cat not in categories:
categories[cat] = {
"weighted_score": 0.0,
"max_weighted": 0.0,
"count": 0,
"full_count": 0,
"partial_count": 0,
"planned_count": 0,
"gap_count": 0,
"effort_hours": 0,
}
categories[cat]["weighted_score"] += req["weighted_score"]
categories[cat]["max_weighted"] += req["max_weighted"]
categories[cat]["count"] += 1
categories[cat]["effort_hours"] += req["effort_hours"]
status_key = f"{req['coverage_status']}_count"
if status_key in categories[cat]:
categories[cat][status_key] += 1
result = {}
for cat, scores in categories.items():
coverage_pct = safe_divide(scores["weighted_score"], scores["max_weighted"]) * 100
result[cat] = {
"coverage_percentage": round(coverage_pct, 1),
"requirements_count": int(scores["count"]),
"full": int(scores["full_count"]),
"partial": int(scores["partial_count"]),
"planned": int(scores["planned_count"]),
"gap": int(scores["gap_count"]),
"effort_hours": int(scores["effort_hours"]),
}
return result
def determine_bid_recommendation(
overall_coverage: float,
must_have_gaps: int,
strategic_value: str,
) -> dict[str, Any]:
"""Determine bid/no-bid recommendation based on coverage and gaps.
Args:
overall_coverage: Overall weighted coverage percentage (0-100).
must_have_gaps: Number of must-have requirements with gap status.
strategic_value: Strategic value assessment (high, medium, low).
Returns:
Recommendation dictionary with decision and rationale.
"""
coverage_ratio = overall_coverage / 100.0
reasons = []
# Primary decision logic
if coverage_ratio >= BID_THRESHOLD and must_have_gaps <= MAX_MUST_HAVE_GAPS_FOR_BID:
decision = "BID"
reasons.append(f"Coverage score {overall_coverage:.1f}% exceeds {BID_THRESHOLD*100:.0f}% threshold")
if must_have_gaps > 0:
reasons.append(f"{must_have_gaps} must-have gap(s) within acceptable range (max {MAX_MUST_HAVE_GAPS_FOR_BID})")
elif coverage_ratio >= CONDITIONAL_THRESHOLD or (
must_have_gaps <= MAX_MUST_HAVE_GAPS_FOR_BID and coverage_ratio >= 0.4
):
decision = "CONDITIONAL BID"
reasons.append(f"Coverage score {overall_coverage:.1f}% in conditional range ({CONDITIONAL_THRESHOLD*100:.0f}%-{BID_THRESHOLD*100:.0f}%)")
if must_have_gaps > 0:
reasons.append(f"{must_have_gaps} must-have gap(s) require mitigation plan")
else:
decision = "NO-BID"
if coverage_ratio < CONDITIONAL_THRESHOLD:
reasons.append(f"Coverage score {overall_coverage:.1f}% below {CONDITIONAL_THRESHOLD*100:.0f}% minimum")
if must_have_gaps > MAX_MUST_HAVE_GAPS_FOR_BID:
reasons.append(f"{must_have_gaps} must-have gaps exceed maximum of {MAX_MUST_HAVE_GAPS_FOR_BID}")
# Strategic value adjustment
if strategic_value.lower() == "high" and decision == "CONDITIONAL BID":
reasons.append("High strategic value supports pursuing despite coverage gaps")
elif strategic_value.lower() == "low" and decision == "CONDITIONAL BID":
decision = "NO-BID"
reasons.append("Low strategic value does not justify investment for conditional coverage")
confidence = "high" if coverage_ratio >= 0.80 else (
"medium" if coverage_ratio >= 0.60 else "low"
)
return {
"decision": decision,
"confidence": confidence,
"overall_coverage_percentage": round(overall_coverage, 1),
"must_have_gaps": must_have_gaps,
"strategic_value": strategic_value,
"reasons": reasons,
}
def generate_risk_assessment(
analyzed_reqs: list[dict[str, Any]],
gaps: list[dict[str, Any]],
) -> list[dict[str, str]]:
"""Generate risk assessment based on gaps and coverage patterns.
Args:
analyzed_reqs: List of analyzed requirement dictionaries.
gaps: List of gap analysis entries.
Returns:
List of risk entries with impact and mitigation.
"""
risks = []
critical_gaps = [g for g in gaps if g["severity"] == "critical"]
if critical_gaps:
risks.append({
"risk": "Critical requirement gaps",
"impact": "high",
"description": f"{len(critical_gaps)} must-have requirements not fully met",
"mitigation": "Prioritize engineering effort or partner integration for gap closure",
})
total_effort = sum(r["effort_hours"] for r in analyzed_reqs if r["coverage_status"] != "full")
if total_effort > 200:
risks.append({
"risk": "High customization effort",
"impact": "high",
"description": f"{total_effort} hours estimated for non-full requirements",
"mitigation": "Evaluate resource availability and timeline feasibility before committing",
})
elif total_effort > 80:
risks.append({
"risk": "Moderate customization effort",
"impact": "medium",
"description": f"{total_effort} hours estimated for non-full requirements",
"mitigation": "Phase implementation and set clear expectations on delivery timeline",
})
planned_count = sum(1 for r in analyzed_reqs if r["coverage_status"] == "planned")
if planned_count > 3:
risks.append({
"risk": "Roadmap dependency",
"impact": "medium",
"description": f"{planned_count} requirements depend on planned product features",
"mitigation": "Confirm roadmap timelines with product team; include contractual commitments if needed",
})
partial_count = sum(1 for r in analyzed_reqs if r["coverage_status"] == "partial")
if partial_count > 5:
risks.append({
"risk": "Workaround complexity",
"impact": "medium",
"description": f"{partial_count} requirements need workarounds or configuration",
"mitigation": "Document workarounds clearly; plan for native support in future releases",
})
if not risks:
risks.append({
"risk": "No significant risks identified",
"impact": "low",
"description": "Strong coverage across all requirement categories",
"mitigation": "Maintain standard engagement process",
})
return risks
def analyze_rfp(data: dict[str, Any]) -> dict[str, Any]:
"""Run the complete RFP analysis pipeline.
Args:
data: Parsed RFP data with requirements array.
Returns:
Complete analysis results dictionary.
"""
rfp_info = {
"rfp_name": data.get("rfp_name", "Unnamed RFP"),
"customer": data.get("customer", "Unknown Customer"),
"due_date": data.get("due_date", "Not specified"),
"strategic_value": data.get("strategic_value", "medium"),
"deal_value": data.get("deal_value", "Not specified"),
}
# Analyze each requirement
analyzed_reqs = [analyze_requirement(req) for req in data["requirements"]]
# Compute overall scores
total_weighted = sum(r["weighted_score"] for r in analyzed_reqs)
total_max = sum(r["max_weighted"] for r in analyzed_reqs)
overall_coverage = safe_divide(total_weighted, total_max) * 100
# Coverage summary
total_count = len(analyzed_reqs)
full_count = sum(1 for r in analyzed_reqs if r["coverage_status"] == "full")
partial_count = sum(1 for r in analyzed_reqs if r["coverage_status"] == "partial")
planned_count = sum(1 for r in analyzed_reqs if r["coverage_status"] == "planned")
gap_count = sum(1 for r in analyzed_reqs if r["coverage_status"] == "gap")
# Must-have gap count
must_have_gaps = sum(
1 for r in analyzed_reqs
if r["priority"] == "must-have" and r["coverage_status"] == "gap"
)
# Category breakdown
category_scores = compute_category_scores(analyzed_reqs)
# Gap analysis
gaps = generate_gap_analysis(analyzed_reqs)
# Bid recommendation
bid_recommendation = determine_bid_recommendation(
overall_coverage,
must_have_gaps,
rfp_info["strategic_value"],
)
# Risk assessment
risks = generate_risk_assessment(analyzed_reqs, gaps)
# Effort summary
total_effort = sum(r["effort_hours"] for r in analyzed_reqs)
gap_effort = sum(r["effort_hours"] for r in analyzed_reqs if r["coverage_status"] != "full")
return {
"rfp_info": rfp_info,
"coverage_summary": {
"overall_coverage_percentage": round(overall_coverage, 1),
"total_requirements": total_count,
"full": full_count,
"partial": partial_count,
"planned": planned_count,
"gap": gap_count,
"must_have_gaps": must_have_gaps,
},
"category_scores": category_scores,
"bid_recommendation": bid_recommendation,
"gap_analysis": gaps,
"risk_assessment": risks,
"effort_estimate": {
"total_hours": total_effort,
"gap_closure_hours": gap_effort,
"full_coverage_hours": total_effort - gap_effort,
},
"requirements_detail": analyzed_reqs,
}
def format_text(result: dict[str, Any]) -> str:
"""Format analysis results as human-readable text.
Args:
result: Complete analysis results dictionary.
Returns:
Formatted text string.
"""
lines = []
info = result["rfp_info"]
lines.append("=" * 70)
lines.append("RFP RESPONSE ANALYSIS")
lines.append("=" * 70)
lines.append(f"RFP: {info['rfp_name']}")
lines.append(f"Customer: {info['customer']}")
lines.append(f"Due Date: {info['due_date']}")
lines.append(f"Deal Value: {info['deal_value']}")
lines.append(f"Strategic Value: {info['strategic_value'].upper()}")
lines.append("")
# Coverage summary
cs = result["coverage_summary"]
lines.append("-" * 70)
lines.append("COVERAGE SUMMARY")
lines.append("-" * 70)
lines.append(f"Overall Coverage: {cs['overall_coverage_percentage']}%")
lines.append(f"Total Requirements: {cs['total_requirements']}")
lines.append(f" Full: {cs['full']} | Partial: {cs['partial']} | Planned: {cs['planned']} | Gap: {cs['gap']}")
lines.append(f"Must-Have Gaps: {cs['must_have_gaps']}")
lines.append("")
# Bid recommendation
bid = result["bid_recommendation"]
lines.append("-" * 70)
lines.append(f"BID RECOMMENDATION: {bid['decision']}")
lines.append(f"Confidence: {bid['confidence'].upper()}")
lines.append("-" * 70)
for reason in bid["reasons"]:
lines.append(f" - {reason}")
lines.append("")
# Category scores
lines.append("-" * 70)
lines.append("CATEGORY BREAKDOWN")
lines.append("-" * 70)
lines.append(f"{'Category':<25} {'Coverage':>8} {'Full':>5} {'Part':>5} {'Plan':>5} {'Gap':>5} {'Effort':>7}")
lines.append("-" * 70)
for cat, scores in result["category_scores"].items():
lines.append(
f"{cat:<25} {scores['coverage_percentage']:>7.1f}% "
f"{scores['full']:>5} {scores['partial']:>5} "
f"{scores['planned']:>5} {scores['gap']:>5} "
f"{scores['effort_hours']:>6}h"
)
lines.append("")
# Gap analysis
gaps = result["gap_analysis"]
if gaps:
lines.append("-" * 70)
lines.append("GAP ANALYSIS")
lines.append("-" * 70)
for gap in gaps:
severity_marker = "!!!" if gap["severity"] == "critical" else (
"!!" if gap["severity"] == "high" else "!"
)
lines.append(f" [{severity_marker}] {gap['id']}: {gap['requirement']}")
lines.append(f" Category: {gap['category']} | Priority: {gap['priority']} | Status: {gap['coverage_status']}")
lines.append(f" Effort: {gap['effort_hours']}h | Mitigation: {gap['mitigation']}")
lines.append("")
# Risk assessment
risks = result["risk_assessment"]
lines.append("-" * 70)
lines.append("RISK ASSESSMENT")
lines.append("-" * 70)
for risk in risks:
lines.append(f" [{risk['impact'].upper()}] {risk['risk']}")
lines.append(f" {risk['description']}")
lines.append(f" Mitigation: {risk['mitigation']}")
lines.append("")
# Effort estimate
effort = result["effort_estimate"]
lines.append("-" * 70)
lines.append("EFFORT ESTIMATE")
lines.append("-" * 70)
lines.append(f" Total Effort: {effort['total_hours']} hours")
lines.append(f" Gap Closure Effort: {effort['gap_closure_hours']} hours")
lines.append(f" Supported Effort: {effort['full_coverage_hours']} hours")
lines.append("")
lines.append("=" * 70)
return "\n".join(lines)
def main() -> None:
"""Main entry point for the RFP Response Analyzer."""
parser = argparse.ArgumentParser(
description="Analyze RFP/RFI requirements for coverage, gaps, and bid recommendation.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Coverage Categories:\n"
" Full (100%) - Requirement fully met\n"
" Partial (50%) - Partially met, workaround needed\n"
" Planned (25%) - On roadmap, not yet available\n"
" Gap (0%) - Not supported\n"
"\n"
"Priority Weights:\n"
" Must-Have (3x) | Should-Have (2x) | Nice-to-Have (1x)\n"
"\n"
"Example:\n"
" python rfp_response_analyzer.py rfp_data.json --format json\n"
),
)
parser.add_argument(
"input_file",
help="Path to JSON file containing RFP requirements data",
)
parser.add_argument(
"--format",
choices=["json", "text"],
default="text",
dest="output_format",
help="Output format: json or text (default: text)",
)
args = parser.parse_args()
data = load_rfp_data(args.input_file)
result = analyze_rfp(data)
if args.output_format == "json":
print(json.dumps(result, indent=2))
else:
print(format_text(result))
if __name__ == "__main__":
main()
Dependency Auditor
---
name: "dependency-auditor"
description: "Dependency Auditor"
---
# Dependency Auditor
> **Skill Type:** POWERFUL
> **Category:** Engineering
> **Domain:** Dependency Management & Security
## Overview
The **Dependency Auditor** is a comprehensive toolkit for analyzing, auditing, and managing dependencies across multi-language software projects. This skill provides deep visibility into your project's dependency ecosystem, enabling teams to identify vulnerabilities, ensure license compliance, optimize dependency trees, and plan safe upgrades.
In modern software development, dependencies form complex webs that can introduce significant security, legal, and maintenance risks. A single project might have hundreds of direct and transitive dependencies, each potentially introducing vulnerabilities, license conflicts, or maintenance burden. This skill addresses these challenges through automated analysis and actionable recommendations.
## Core Capabilities
### 1. Vulnerability Scanning & CVE Matching
**Comprehensive Security Analysis**
- Scans dependencies against built-in vulnerability databases
- Matches Common Vulnerabilities and Exposures (CVE) patterns
- Identifies known security issues across multiple ecosystems
- Analyzes transitive dependency vulnerabilities
- Provides CVSS scores and exploit assessments
- Tracks vulnerability disclosure timelines
- Maps vulnerabilities to dependency paths
**Multi-Language Support**
- **JavaScript/Node.js**: package.json, package-lock.json, yarn.lock
- **Python**: requirements.txt, pyproject.toml, Pipfile.lock, poetry.lock
- **Go**: go.mod, go.sum
- **Rust**: Cargo.toml, Cargo.lock
- **Ruby**: Gemfile, Gemfile.lock
- **Java/Maven**: pom.xml, gradle.lockfile
- **PHP**: composer.json, composer.lock
- **C#/.NET**: packages.config, project.assets.json
### 2. License Compliance & Legal Risk Assessment
**License Classification System**
- **Permissive Licenses**: MIT, Apache 2.0, BSD (2-clause, 3-clause), ISC
- **Copyleft (Strong)**: GPL (v2, v3), AGPL (v3)
- **Copyleft (Weak)**: LGPL (v2.1, v3), MPL (v2.0)
- **Proprietary**: Commercial, custom, or restrictive licenses
- **Dual Licensed**: Multi-license scenarios and compatibility
- **Unknown/Ambiguous**: Missing or unclear licensing
**Conflict Detection**
- Identifies incompatible license combinations
- Warns about GPL contamination in permissive projects
- Analyzes license inheritance through dependency chains
- Provides compliance recommendations for distribution
- Generates legal risk matrices for decision-making
### 3. Outdated Dependency Detection
**Version Analysis**
- Identifies dependencies with available updates
- Categorizes updates by severity (patch, minor, major)
- Detects pinned versions that may be outdated
- Analyzes semantic versioning patterns
- Identifies floating version specifiers
- Tracks release frequencies and maintenance status
**Maintenance Status Assessment**
- Identifies abandoned or unmaintained packages
- Analyzes commit frequency and contributor activity
- Tracks last release dates and security patch availability
- Identifies packages with known end-of-life dates
- Assesses upstream maintenance quality
### 4. Dependency Bloat Analysis
**Unused Dependency Detection**
- Identifies dependencies that aren't actually imported/used
- Analyzes import statements and usage patterns
- Detects redundant dependencies with overlapping functionality
- Identifies oversized packages for simple use cases
- Maps actual vs. declared dependency usage
**Redundancy Analysis**
- Identifies multiple packages providing similar functionality
- Detects version conflicts in transitive dependencies
- Analyzes bundle size impact of dependencies
- Identifies opportunities for dependency consolidation
- Maps dependency overlap and duplication
### 5. Upgrade Path Planning & Breaking Change Risk
**Semantic Versioning Analysis**
- Analyzes semver patterns to predict breaking changes
- Identifies safe upgrade paths (patch/minor versions)
- Flags major version updates requiring attention
- Tracks breaking changes across dependency updates
- Provides rollback strategies for failed upgrades
**Risk Assessment Matrix**
- Low Risk: Patch updates, security fixes
- Medium Risk: Minor updates with new features
- High Risk: Major version updates, API changes
- Critical Risk: Dependencies with known breaking changes
**Upgrade Prioritization**
- Security patches: Highest priority
- Bug fixes: High priority
- Feature updates: Medium priority
- Major rewrites: Planned priority
- Deprecated features: Immediate attention
### 6. Supply Chain Security
**Dependency Provenance**
- Verifies package signatures and checksums
- Analyzes package download sources and mirrors
- Identifies suspicious or compromised packages
- Tracks package ownership changes and maintainer shifts
- Detects typosquatting and malicious packages
**Transitive Risk Analysis**
- Maps complete dependency trees
- Identifies high-risk transitive dependencies
- Analyzes dependency depth and complexity
- Tracks influence of indirect dependencies
- Provides supply chain risk scoring
### 7. Lockfile Analysis & Deterministic Builds
**Lockfile Validation**
- Ensures lockfiles are up-to-date with manifests
- Validates integrity hashes and version consistency
- Identifies drift between environments
- Analyzes lockfile conflicts and resolution strategies
- Ensures deterministic, reproducible builds
**Environment Consistency**
- Compares dependencies across environments (dev/staging/prod)
- Identifies version mismatches between team members
- Validates CI/CD environment consistency
- Tracks dependency resolution differences
## Technical Architecture
### Scanner Engine (`dep_scanner.py`)
- Multi-format parser supporting 8+ package ecosystems
- Built-in vulnerability database with 500+ CVE patterns
- Transitive dependency resolution from lockfiles
- JSON and human-readable output formats
- Configurable scanning depth and exclusion patterns
### License Analyzer (`license_checker.py`)
- License detection from package metadata and files
- Compatibility matrix with 20+ license types
- Conflict detection engine with remediation suggestions
- Risk scoring based on distribution and usage context
- Export capabilities for legal review
### Upgrade Planner (`upgrade_planner.py`)
- Semantic version analysis with breaking change prediction
- Dependency ordering based on risk and interdependence
- Migration checklists with testing recommendations
- Rollback procedures for failed upgrades
- Timeline estimation for upgrade cycles
## Use Cases & Applications
### Security Teams
- **Vulnerability Management**: Continuous scanning for security issues
- **Incident Response**: Rapid assessment of vulnerable dependencies
- **Supply Chain Monitoring**: Tracking third-party security posture
- **Compliance Reporting**: Automated security compliance documentation
### Legal & Compliance Teams
- **License Auditing**: Comprehensive license compliance verification
- **Risk Assessment**: Legal risk analysis for software distribution
- **Due Diligence**: Dependency licensing for M&A activities
- **Policy Enforcement**: Automated license policy compliance
### Development Teams
- **Dependency Hygiene**: Regular cleanup of unused dependencies
- **Upgrade Planning**: Strategic dependency update scheduling
- **Performance Optimization**: Bundle size optimization through dep analysis
- **Technical Debt**: Identifying and prioritizing dependency technical debt
### DevOps & Platform Teams
- **Build Optimization**: Faster builds through dependency optimization
- **Security Automation**: Automated vulnerability scanning in CI/CD
- **Environment Consistency**: Ensuring consistent dependencies across environments
- **Release Management**: Dependency-aware release planning
## Integration Patterns
### CI/CD Pipeline Integration
```bash
# Security gate in CI
python dep_scanner.py /project --format json --fail-on-high
python license_checker.py /project --policy strict --format json
```
### Scheduled Audits
```bash
# Weekly dependency audit
./audit_dependencies.sh > weekly_report.html
python upgrade_planner.py deps.json --timeline 30days
```
### Development Workflow
```bash
# Pre-commit dependency check
python dep_scanner.py . --quick-scan
python license_checker.py . --warn-conflicts
```
## Advanced Features
### Custom Vulnerability Databases
- Support for internal/proprietary vulnerability feeds
- Custom CVE pattern definitions
- Organization-specific risk scoring
- Integration with enterprise security tools
### Policy-Based Scanning
- Configurable license policies by project type
- Custom risk thresholds and escalation rules
- Automated policy enforcement and notifications
- Exception management for approved violations
### Reporting & Dashboards
- Executive summaries for management
- Technical reports for development teams
- Trend analysis and dependency health metrics
- Integration with project management tools
### Multi-Project Analysis
- Portfolio-level dependency analysis
- Shared dependency impact analysis
- Organization-wide license compliance
- Cross-project vulnerability propagation
## Best Practices
### Scanning Frequency
- **Security Scans**: Daily or on every commit
- **License Audits**: Weekly or monthly
- **Upgrade Planning**: Monthly or quarterly
- **Full Dependency Audit**: Quarterly
### Risk Management
1. **Prioritize Security**: Address high/critical CVEs immediately
2. **License First**: Ensure compliance before functionality
3. **Gradual Updates**: Incremental dependency updates
4. **Test Thoroughly**: Comprehensive testing after updates
5. **Monitor Continuously**: Automated monitoring and alerting
### Team Workflows
1. **Security Champions**: Designate dependency security owners
2. **Review Process**: Mandatory review for new dependencies
3. **Update Cycles**: Regular, scheduled dependency updates
4. **Documentation**: Maintain dependency rationale and decisions
5. **Training**: Regular team education on dependency security
## Metrics & KPIs
### Security Metrics
- Mean Time to Patch (MTTP) for vulnerabilities
- Number of high/critical vulnerabilities
- Percentage of dependencies with known vulnerabilities
- Security debt accumulation rate
### Compliance Metrics
- License compliance percentage
- Number of license conflicts
- Time to resolve compliance issues
- Policy violation frequency
### Maintenance Metrics
- Percentage of up-to-date dependencies
- Average dependency age
- Number of abandoned dependencies
- Upgrade success rate
### Efficiency Metrics
- Bundle size reduction percentage
- Unused dependency elimination rate
- Build time improvement
- Developer productivity impact
## Troubleshooting Guide
### Common Issues
1. **False Positives**: Tuning vulnerability detection sensitivity
2. **License Ambiguity**: Resolving unclear or multiple licenses
3. **Breaking Changes**: Managing major version upgrades
4. **Performance Impact**: Optimizing scanning for large codebases
### Resolution Strategies
- Whitelist false positives with documentation
- Contact maintainers for license clarification
- Implement feature flags for risky upgrades
- Use incremental scanning for large projects
## Future Enhancements
### Planned Features
- Machine learning for vulnerability prediction
- Automated dependency update pull requests
- Integration with container image scanning
- Real-time dependency monitoring dashboards
- Natural language policy definition
### Ecosystem Expansion
- Additional language support (Swift, Kotlin, Dart)
- Container and infrastructure dependencies
- Development tool and build system dependencies
- Cloud service and SaaS dependency tracking
---
## Quick Start
```bash
# Scan project for vulnerabilities and licenses
python scripts/dep_scanner.py /path/to/project
# Check license compliance
python scripts/license_checker.py /path/to/project --policy strict
# Plan dependency upgrades
python scripts/upgrade_planner.py deps.json --risk-threshold medium
```
For detailed usage instructions, see [README.md](README.md).
---
*This skill provides comprehensive dependency management capabilities essential for maintaining secure, compliant, and efficient software projects. Regular use helps teams stay ahead of security threats, maintain legal compliance, and optimize their dependency ecosystems.*
FILE:README.md
# Dependency Auditor
A comprehensive toolkit for analyzing, auditing, and managing dependencies across multi-language software projects. This skill provides vulnerability scanning, license compliance checking, and upgrade path planning with zero external dependencies.
## Overview
The Dependency Auditor skill consists of three main Python scripts that work together to provide complete dependency management capabilities:
- **`dep_scanner.py`**: Vulnerability scanning and dependency analysis
- **`license_checker.py`**: License compliance and conflict detection
- **`upgrade_planner.py`**: Upgrade path planning and risk assessment
## Features
### 🔍 Vulnerability Scanning
- Multi-language dependency parsing (JavaScript, Python, Go, Rust, Ruby, Java)
- Built-in vulnerability database with common CVE patterns
- CVSS scoring and risk assessment
- JSON and human-readable output formats
- CI/CD integration support
### ⚖️ License Compliance
- Comprehensive license classification and compatibility analysis
- Automatic conflict detection between project and dependency licenses
- Risk assessment for commercial usage and distribution
- Compliance scoring and reporting
### 📈 Upgrade Planning
- Semantic versioning analysis with breaking change prediction
- Risk-based upgrade prioritization
- Phased migration plans with rollback procedures
- Security-focused upgrade recommendations
## Installation
No external dependencies required! All scripts use only Python standard library.
```bash
# Clone or download the dependency-auditor skill
cd engineering/dependency-auditor/scripts
# Make scripts executable
chmod +x dep_scanner.py license_checker.py upgrade_planner.py
```
## Quick Start
### 1. Scan for Vulnerabilities
```bash
# Basic vulnerability scan
python dep_scanner.py /path/to/your/project
# JSON output for automation
python dep_scanner.py /path/to/your/project --format json --output scan_results.json
# Fail CI/CD on high-severity vulnerabilities
python dep_scanner.py /path/to/your/project --fail-on-high
```
### 2. Check License Compliance
```bash
# Basic license compliance check
python license_checker.py /path/to/your/project
# Strict policy enforcement
python license_checker.py /path/to/your/project --policy strict
# Use existing dependency inventory
python license_checker.py /path/to/project --inventory scan_results.json --format json
```
### 3. Plan Dependency Upgrades
```bash
# Generate upgrade plan from dependency inventory
python upgrade_planner.py scan_results.json
# Custom timeline and risk filtering
python upgrade_planner.py scan_results.json --timeline 60 --risk-threshold medium
# Security updates only
python upgrade_planner.py scan_results.json --security-only --format json
```
## Detailed Usage
### Dependency Scanner (`dep_scanner.py`)
The dependency scanner parses project files to extract dependencies and check them against a built-in vulnerability database.
#### Supported File Formats
- **JavaScript/Node.js**: package.json, package-lock.json, yarn.lock
- **Python**: requirements.txt, pyproject.toml, Pipfile.lock, poetry.lock
- **Go**: go.mod, go.sum
- **Rust**: Cargo.toml, Cargo.lock
- **Ruby**: Gemfile, Gemfile.lock
#### Command Line Options
```bash
python dep_scanner.py [PROJECT_PATH] [OPTIONS]
Required Arguments:
PROJECT_PATH Path to the project directory to scan
Optional Arguments:
--format {text,json} Output format (default: text)
--output FILE Output file path (default: stdout)
--fail-on-high Exit with error code if high-severity vulnerabilities found
--quick-scan Perform quick scan (skip transitive dependencies)
Examples:
python dep_scanner.py /app
python dep_scanner.py . --format json --output results.json
python dep_scanner.py /project --fail-on-high --quick-scan
```
#### Output Format
**Text Output:**
```
============================================================
DEPENDENCY SECURITY SCAN REPORT
============================================================
Scan Date: 2024-02-16T15:30:00.000Z
Project: /example/sample-web-app
SUMMARY:
Total Dependencies: 23
Unique Dependencies: 19
Ecosystems: npm
Vulnerabilities Found: 1
High Severity: 1
Medium Severity: 0
Low Severity: 0
VULNERABLE DEPENDENCIES:
------------------------------
Package: lodash v4.17.20 (npm)
• CVE-2021-23337: Prototype pollution in lodash
Severity: HIGH (CVSS: 7.2)
Fixed in: 4.17.21
RECOMMENDATIONS:
--------------------
1. URGENT: Address 1 high-severity vulnerabilities immediately
2. Update lodash from 4.17.20 to 4.17.21 to fix CVE-2021-23337
```
**JSON Output:**
```json
{
"timestamp": "2024-02-16T15:30:00.000Z",
"project_path": "/example/sample-web-app",
"dependencies": [
{
"name": "lodash",
"version": "4.17.20",
"ecosystem": "npm",
"direct": true,
"vulnerabilities": [
{
"id": "CVE-2021-23337",
"summary": "Prototype pollution in lodash",
"severity": "HIGH",
"cvss_score": 7.2
}
]
}
],
"recommendations": [
"Update lodash from 4.17.20 to 4.17.21 to fix CVE-2021-23337"
]
}
```
### License Checker (`license_checker.py`)
The license checker analyzes dependency licenses for compliance and detects potential conflicts.
#### Command Line Options
```bash
python license_checker.py [PROJECT_PATH] [OPTIONS]
Required Arguments:
PROJECT_PATH Path to the project directory to analyze
Optional Arguments:
--inventory FILE Path to dependency inventory JSON file
--format {text,json} Output format (default: text)
--output FILE Output file path (default: stdout)
--policy {permissive,strict} License policy strictness (default: permissive)
--warn-conflicts Show warnings for potential conflicts
Examples:
python license_checker.py /app
python license_checker.py . --format json --output compliance.json
python license_checker.py /app --inventory deps.json --policy strict
```
#### License Classifications
The tool classifies licenses into risk categories:
- **Permissive (Low Risk)**: MIT, Apache-2.0, BSD, ISC
- **Weak Copyleft (Medium Risk)**: LGPL, MPL
- **Strong Copyleft (High Risk)**: GPL, AGPL
- **Proprietary (High Risk)**: Commercial licenses
- **Unknown (Critical Risk)**: Unidentified licenses
#### Compatibility Matrix
The tool includes a comprehensive compatibility matrix that checks:
- Project license vs. dependency licenses
- GPL contamination detection
- Commercial usage restrictions
- Distribution requirements
### Upgrade Planner (`upgrade_planner.py`)
The upgrade planner analyzes dependency inventories and creates prioritized upgrade plans.
#### Command Line Options
```bash
python upgrade_planner.py [INVENTORY_FILE] [OPTIONS]
Required Arguments:
INVENTORY_FILE Path to dependency inventory JSON file
Optional Arguments:
--timeline DAYS Timeline for upgrade plan in days (default: 90)
--format {text,json} Output format (default: text)
--output FILE Output file path (default: stdout)
--risk-threshold {safe,low,medium,high,critical} Maximum risk level (default: high)
--security-only Only plan upgrades with security fixes
Examples:
python upgrade_planner.py deps.json
python upgrade_planner.py inventory.json --timeline 60 --format json
python upgrade_planner.py deps.json --security-only --risk-threshold medium
```
#### Risk Assessment
Upgrades are classified by risk level:
- **Safe**: Patch updates with no breaking changes
- **Low**: Minor updates with backward compatibility
- **Medium**: Updates with potential API changes
- **High**: Major version updates with breaking changes
- **Critical**: Updates affecting core functionality
#### Phased Planning
The tool creates three-phase upgrade plans:
1. **Phase 1 (30% of timeline)**: Security fixes and safe updates
2. **Phase 2 (40% of timeline)**: Regular maintenance updates
3. **Phase 3 (30% of timeline)**: Major updates requiring careful planning
## Integration Examples
### CI/CD Pipeline Integration
#### GitHub Actions Example
```yaml
name: Dependency Audit
on: [push, pull_request, schedule]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Run Vulnerability Scan
run: |
python scripts/dep_scanner.py . --format json --output scan.json
python scripts/dep_scanner.py . --fail-on-high
- name: Check License Compliance
run: |
python scripts/license_checker.py . --inventory scan.json --policy strict
- name: Generate Upgrade Plan
run: |
python scripts/upgrade_planner.py scan.json --output upgrade-plan.txt
- name: Upload Reports
uses: actions/upload-artifact@v3
with:
name: dependency-reports
path: |
scan.json
upgrade-plan.txt
```
#### Jenkins Pipeline Example
```groovy
pipeline {
agent any
stages {
stage('Dependency Audit') {
steps {
script {
// Vulnerability scan
sh 'python scripts/dep_scanner.py . --format json --output scan.json'
// License compliance
sh 'python scripts/license_checker.py . --inventory scan.json --format json --output compliance.json'
// Upgrade planning
sh 'python scripts/upgrade_planner.py scan.json --format json --output upgrades.json'
}
// Archive reports
archiveArtifacts artifacts: '*.json', fingerprint: true
// Fail build on high-severity vulnerabilities
sh 'python scripts/dep_scanner.py . --fail-on-high'
}
}
}
post {
always {
// Publish reports
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: '.',
reportFiles: '*.json',
reportName: 'Dependency Audit Report'
])
}
}
}
```
### Automated Dependency Updates
#### Weekly Security Updates Script
```bash
#!/bin/bash
# weekly-security-updates.sh
set -e
echo "Running weekly security dependency updates..."
# Scan for vulnerabilities
python scripts/dep_scanner.py . --format json --output current-scan.json
# Generate security-only upgrade plan
python scripts/upgrade_planner.py current-scan.json --security-only --output security-upgrades.txt
# Check if security updates are available
if grep -q "URGENT" security-upgrades.txt; then
echo "Security updates found! Creating automated PR..."
# Create branch
git checkout -b "automated-security-updates-$(date +%Y%m%d)"
# Apply updates (example for npm)
npm audit fix --only=prod
# Commit and push
git add .
git commit -m "chore: automated security dependency updates"
git push origin HEAD
# Create PR (using GitHub CLI)
gh pr create \
--title "Automated Security Updates" \
--body-file security-upgrades.txt \
--label "security,dependencies,automated"
else
echo "No critical security updates found."
fi
```
## Sample Files
The `assets/` directory contains sample dependency files for testing:
- `sample_package.json`: Node.js project with various dependencies
- `sample_requirements.txt`: Python project dependencies
- `sample_go.mod`: Go module dependencies
The `expected_outputs/` directory contains example reports showing the expected format and content.
## Advanced Usage
### Custom Vulnerability Database
You can extend the built-in vulnerability database by modifying the `_load_vulnerability_database()` method in `dep_scanner.py`:
```python
def _load_vulnerability_database(self):
"""Load vulnerability database from multiple sources."""
db = self._load_builtin_database()
# Load custom vulnerabilities
custom_db_path = os.environ.get('CUSTOM_VULN_DB')
if custom_db_path and os.path.exists(custom_db_path):
with open(custom_db_path, 'r') as f:
custom_vulns = json.load(f)
db.update(custom_vulns)
return db
```
### Custom License Policies
Create custom license policies by modifying the license database:
```python
# Add custom license
custom_license = LicenseInfo(
name='Custom Internal License',
spdx_id='CUSTOM-1.0',
license_type=LicenseType.PROPRIETARY,
risk_level=RiskLevel.HIGH,
description='Internal company license',
restrictions=['Internal use only'],
obligations=['Attribution required']
)
```
### Multi-Project Analysis
For analyzing multiple projects, create a wrapper script:
```python
#!/usr/bin/env python3
import os
import json
from pathlib import Path
projects = ['/path/to/project1', '/path/to/project2', '/path/to/project3']
results = {}
for project in projects:
project_name = Path(project).name
# Run vulnerability scan
scan_result = subprocess.run([
'python', 'scripts/dep_scanner.py',
project, '--format', 'json'
], capture_output=True, text=True)
if scan_result.returncode == 0:
results[project_name] = json.loads(scan_result.stdout)
# Generate consolidated report
with open('consolidated-report.json', 'w') as f:
json.dump(results, f, indent=2)
```
## Troubleshooting
### Common Issues
1. **Permission Errors**
```bash
chmod +x scripts/*.py
```
2. **Python Version Compatibility**
- Requires Python 3.7 or higher
- Uses only standard library modules
3. **Large Projects**
- Use `--quick-scan` for faster analysis
- Consider excluding large node_modules directories
4. **False Positives**
- Review vulnerability matches manually
- Consider version range parsing improvements
### Debug Mode
Enable debug logging by setting environment variable:
```bash
export DEPENDENCY_AUDIT_DEBUG=1
python scripts/dep_scanner.py /your/project
```
## Contributing
1. **Adding New Package Managers**: Extend the `supported_files` dictionary and add corresponding parsers
2. **Vulnerability Database**: Add new CVE entries to the built-in database
3. **License Support**: Add new license types to the license database
4. **Risk Assessment**: Improve risk scoring algorithms
## References
- [SKILL.md](SKILL.md): Comprehensive skill documentation
- [references/](references/): Best practices and compatibility guides
- [assets/](assets/): Sample dependency files for testing
- [expected_outputs/](expected_outputs/): Example reports and outputs
## License
This skill is licensed under the MIT License. See the project license file for details.
---
**Note**: This tool provides automated analysis to assist with dependency management decisions. Always review recommendations and consult with security and legal teams for critical applications.
FILE:assets/sample_package.json
{
"name": "sample-web-app",
"version": "1.2.3",
"description": "A sample web application with various dependencies for testing dependency auditing",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"build": "webpack --mode production",
"test": "jest",
"lint": "eslint src/",
"audit": "npm audit"
},
"keywords": ["web", "app", "sample", "dependency", "audit"],
"author": "Claude Skills Team",
"license": "MIT",
"dependencies": {
"express": "4.18.1",
"lodash": "4.17.20",
"axios": "1.5.0",
"jsonwebtoken": "8.5.1",
"bcrypt": "5.1.0",
"mongoose": "6.10.0",
"cors": "2.8.5",
"helmet": "6.1.5",
"winston": "3.8.2",
"dotenv": "16.0.3",
"express-rate-limit": "6.7.0",
"multer": "1.4.5-lts.1",
"sharp": "0.32.1",
"nodemailer": "6.9.1",
"socket.io": "4.6.1",
"redis": "4.6.5",
"moment": "2.29.4",
"chalk": "4.1.2",
"commander": "9.4.1"
},
"devDependencies": {
"nodemon": "2.0.22",
"jest": "29.5.0",
"supertest": "6.3.3",
"eslint": "8.40.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-plugin-import": "2.27.5",
"webpack": "5.82.1",
"webpack-cli": "5.1.1",
"babel-loader": "9.1.2",
"@babel/core": "7.22.1",
"@babel/preset-env": "7.22.2",
"css-loader": "6.7.4",
"style-loader": "3.3.3",
"html-webpack-plugin": "5.5.1",
"mini-css-extract-plugin": "2.7.6",
"postcss": "8.4.23",
"postcss-loader": "7.3.0",
"autoprefixer": "10.4.14",
"cross-env": "7.0.3",
"rimraf": "5.0.1"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/example/sample-web-app.git"
},
"bugs": {
"url": "https://github.com/example/sample-web-app/issues"
},
"homepage": "https://github.com/example/sample-web-app#readme"
}
FILE:assets/sample_requirements.txt
# Core web framework
Django==4.1.7
djangorestframework==3.14.0
django-cors-headers==3.14.0
django-environ==0.10.0
django-extensions==3.2.1
# Database and ORM
psycopg2-binary==2.9.6
redis==4.5.4
celery==5.2.7
# Authentication and Security
django-allauth==0.54.0
djangorestframework-simplejwt==5.2.2
cryptography==40.0.1
bcrypt==4.0.1
# HTTP and API clients
requests==2.28.2
httpx==0.24.1
urllib3==1.26.15
# Data processing and analysis
pandas==2.0.1
numpy==1.24.3
Pillow==9.5.0
openpyxl==3.1.2
# Monitoring and logging
sentry-sdk==1.21.1
structlog==23.1.0
# Testing
pytest==7.3.1
pytest-django==4.5.2
pytest-cov==4.0.0
factory-boy==3.2.1
freezegun==1.2.2
# Development tools
black==23.3.0
flake8==6.0.0
isort==5.12.0
pre-commit==3.3.2
django-debug-toolbar==4.0.0
# Documentation
Sphinx==6.2.1
sphinx-rtd-theme==1.2.0
# Deployment and server
gunicorn==20.1.0
whitenoise==6.4.0
# Environment and configuration
python-decouple==3.8
pyyaml==6.0
# Utilities
click==8.1.3
python-dateutil==2.8.2
pytz==2023.3
six==1.16.0
# AWS integration
boto3==1.26.137
botocore==1.29.137
# Email
django-anymail==10.0
FILE:expected_outputs/sample_license_report.txt
============================================================
LICENSE COMPLIANCE REPORT
============================================================
Analysis Date: 2024-02-16T15:30:00.000Z
Project: /example/sample-web-app
Project License: MIT
SUMMARY:
Total Dependencies: 23
Compliance Score: 92.5/100
Overall Risk: LOW
License Conflicts: 0
LICENSE DISTRIBUTION:
Permissive: 21
Copyleft_weak: 1
Copyleft_strong: 0
Proprietary: 0
Unknown: 1
RISK BREAKDOWN:
Low: 21
Medium: 1
High: 0
Critical: 1
HIGH-RISK DEPENDENCIES:
------------------------------
moment v2.29.4: Unknown (CRITICAL)
RECOMMENDATIONS:
--------------------
1. Investigate and clarify licenses for 1 dependencies with unknown licensing
2. Overall compliance score is high - maintain current practices
3. Consider updating moment.js which has been deprecated by maintainers
============================================================
FILE:expected_outputs/sample_upgrade_plan.txt
============================================================
DEPENDENCY UPGRADE PLAN
============================================================
Generated: 2024-02-16T15:30:00.000Z
Timeline: 90 days
UPGRADE SUMMARY:
Total Upgrades Available: 12
Security Updates: 2
Major Version Updates: 3
High Risk Updates: 2
RISK ASSESSMENT:
Overall Risk Level: MEDIUM
Key Risk Factors:
• 2 critical risk upgrades requiring careful planning
• Core framework upgrades: ['express', 'webpack', 'eslint']
• 1 major version upgrades with potential breaking changes
TOP PRIORITY UPGRADES:
------------------------------
🔒 lodash: 4.17.20 → 4.17.21 🔒
Type: Patch | Risk: Low | Priority: 95.0
Security: CVE-2021-23337: Prototype pollution vulnerability
🟡 express: 4.18.1 → 4.18.2
Type: Patch | Risk: Low | Priority: 85.0
🟡 webpack: 5.82.1 → 5.88.0
Type: Minor | Risk: Medium | Priority: 75.0
🔴 eslint: 8.40.0 → 9.0.0
Type: Major | Risk: High | Priority: 65.0
🟢 cors: 2.8.5 → 2.8.7
Type: Patch | Risk: Safe | Priority: 80.0
PHASED UPGRADE PLANS:
------------------------------
Phase 1: Security & Safe Updates (30 days)
Dependencies: lodash, cors, helmet, dotenv, bcrypt
Key Steps: Create feature branch; Update dependency versions in manifest files; Run dependency install/update commands
Phase 2: Regular Updates (36 days)
Dependencies: express, axios, winston, multer
Key Steps: Create feature branch; Update dependency versions in manifest files; Run dependency install/update commands
Phase 3: Major Updates (30 days)
Dependencies: webpack, eslint, jest
... and 2 more
Key Steps: Create feature branch; Update dependency versions in manifest files; Run dependency install/update commands
RECOMMENDATIONS:
--------------------
1. URGENT: 2 security updates available - prioritize immediately
2. Quick wins: 6 safe updates can be applied with minimal risk
3. Plan carefully: 2 high-risk upgrades need thorough testing
============================================================
FILE:expected_outputs/sample_vulnerability_report.json
{
"timestamp": "2024-02-16T15:30:00.000Z",
"project_path": "/example/sample-web-app",
"dependencies": [
{
"name": "lodash",
"version": "4.17.20",
"ecosystem": "npm",
"direct": true,
"license": "MIT",
"vulnerabilities": [
{
"id": "CVE-2021-23337",
"summary": "Prototype pollution in lodash",
"severity": "HIGH",
"cvss_score": 7.2,
"affected_versions": "<4.17.21",
"fixed_version": "4.17.21",
"published_date": "2021-02-15",
"references": [
"https://nvd.nist.gov/vuln/detail/CVE-2021-23337"
]
}
]
},
{
"name": "axios",
"version": "1.5.0",
"ecosystem": "npm",
"direct": true,
"license": "MIT",
"vulnerabilities": []
},
{
"name": "express",
"version": "4.18.1",
"ecosystem": "npm",
"direct": true,
"license": "MIT",
"vulnerabilities": []
},
{
"name": "jsonwebtoken",
"version": "8.5.1",
"ecosystem": "npm",
"direct": true,
"license": "MIT",
"vulnerabilities": []
}
],
"vulnerabilities_found": 1,
"high_severity_count": 1,
"medium_severity_count": 0,
"low_severity_count": 0,
"ecosystems": ["npm"],
"scan_summary": {
"total_dependencies": 4,
"unique_dependencies": 4,
"ecosystems_found": 1,
"vulnerable_dependencies": 1,
"vulnerability_breakdown": {
"high": 1,
"medium": 0,
"low": 0
}
},
"recommendations": [
"URGENT: Address 1 high-severity vulnerabilities immediately",
"Update lodash from 4.17.20 to 4.17.21 to fix CVE-2021-23337"
]
}
FILE:references/dependency_management_best_practices.md
# Dependency Management Best Practices
A comprehensive guide to effective dependency management across the software development lifecycle, covering strategy, governance, security, and operational practices.
## Strategic Foundation
### Dependency Strategy
#### Philosophy and Principles
1. **Minimize Dependencies**: Every dependency is a liability
- Prefer standard library solutions when possible
- Evaluate alternatives before adding new dependencies
- Regularly audit and remove unused dependencies
2. **Quality Over Convenience**: Choose well-maintained, secure dependencies
- Active maintenance and community
- Strong security track record
- Comprehensive documentation and testing
3. **Stability Over Novelty**: Prefer proven, stable solutions
- Avoid dependencies with frequent breaking changes
- Consider long-term support and backwards compatibility
- Evaluate dependency maturity and adoption
4. **Transparency and Control**: Understand what you're depending on
- Review dependency source code when possible
- Understand licensing implications
- Monitor dependency behavior and updates
#### Decision Framework
##### Evaluation Criteria
```
Dependency Evaluation Scorecard:
│
├── Necessity (25 points)
│ ├── Problem complexity (10)
│ ├── Standard library alternatives (8)
│ └── Internal implementation effort (7)
│
├── Quality (30 points)
│ ├── Code quality and architecture (10)
│ ├── Test coverage and reliability (10)
│ └── Documentation completeness (10)
│
├── Maintenance (25 points)
│ ├── Active development and releases (10)
│ ├── Issue response time (8)
│ └── Community size and engagement (7)
│
└── Compatibility (20 points)
├── License compatibility (10)
├── Version stability (5)
└── Platform/runtime compatibility (5)
Scoring:
- 80-100: Excellent choice
- 60-79: Good choice with monitoring
- 40-59: Acceptable with caution
- Below 40: Avoid or find alternatives
```
### Governance Framework
#### Dependency Approval Process
##### New Dependency Approval
```
New Dependency Workflow:
│
1. Developer identifies need
├── Documents use case and requirements
├── Researches available options
└── Proposes recommendation
↓
2. Technical review
├── Architecture team evaluates fit
├── Security team assesses risks
└── Legal team reviews licensing
↓
3. Management approval
├── Low risk: Tech lead approval
├── Medium risk: Architecture board
└── High risk: CTO approval
↓
4. Implementation
├── Add to approved dependencies list
├── Document usage guidelines
└── Configure monitoring and alerts
```
##### Risk Classification
- **Low Risk**: Well-known libraries, permissive licenses, stable APIs
- **Medium Risk**: Less common libraries, weak copyleft licenses, evolving APIs
- **High Risk**: New/experimental libraries, strong copyleft licenses, breaking changes
#### Dependency Policies
##### Licensing Policy
```yaml
licensing_policy:
allowed_licenses:
- MIT
- Apache-2.0
- BSD-3-Clause
- BSD-2-Clause
- ISC
conditional_licenses:
- LGPL-2.1 # Library linking only
- LGPL-3.0 # With legal review
- MPL-2.0 # File-level copyleft acceptable
prohibited_licenses:
- GPL-2.0 # Strong copyleft
- GPL-3.0 # Strong copyleft
- AGPL-3.0 # Network copyleft
- SSPL # Server-side public license
- Custom # Unknown/proprietary licenses
exceptions:
process: "Legal and executive approval required"
documentation: "Risk assessment and mitigation plan"
```
##### Security Policy
```yaml
security_policy:
vulnerability_response:
critical: "24 hours"
high: "1 week"
medium: "1 month"
low: "Next release cycle"
scanning_requirements:
frequency: "Daily automated scans"
tools: ["Snyk", "OWASP Dependency Check"]
ci_cd_integration: "Mandatory security gates"
approval_thresholds:
known_vulnerabilities: "Zero tolerance for high/critical"
maintenance_status: "Must be actively maintained"
community_size: "Minimum 10 contributors or enterprise backing"
```
## Operational Practices
### Dependency Lifecycle Management
#### Addition Process
1. **Research and Evaluation**
```bash
# Example evaluation script
#!/bin/bash
PACKAGE=$1
echo "=== Package Analysis: $PACKAGE ==="
# Check package stats
npm view $PACKAGE
# Security audit
npm audit $PACKAGE
# License check
npm view $PACKAGE license
# Dependency tree
npm ls $PACKAGE
# Recent activity
npm view $PACKAGE --json | jq '.time'
```
2. **Documentation Requirements**
- **Purpose**: Why this dependency is needed
- **Alternatives**: Other options considered and why rejected
- **Risk Assessment**: Security, licensing, maintenance risks
- **Usage Guidelines**: How to use safely within the project
- **Exit Strategy**: How to remove/replace if needed
3. **Integration Standards**
- Pin to specific versions (avoid wildcards)
- Document version constraints and reasoning
- Configure automated update policies
- Add monitoring and alerting
#### Update Management
##### Update Strategy
```
Update Prioritization:
│
├── Security Updates (P0)
│ ├── Critical vulnerabilities: Immediate
│ ├── High vulnerabilities: Within 1 week
│ └── Medium vulnerabilities: Within 1 month
│
├── Maintenance Updates (P1)
│ ├── Bug fixes: Next minor release
│ ├── Performance improvements: Next minor release
│ └── Deprecation warnings: Plan for major release
│
└── Feature Updates (P2)
├── Minor versions: Quarterly review
├── Major versions: Annual planning cycle
└── Breaking changes: Dedicated migration projects
```
##### Update Process
```yaml
update_workflow:
automated:
patch_updates:
enabled: true
auto_merge: true
conditions:
- tests_pass: true
- security_scan_clean: true
- no_breaking_changes: true
minor_updates:
enabled: true
auto_merge: false
requires: "Manual review and testing"
major_updates:
enabled: false
requires: "Full impact assessment and planning"
testing_requirements:
unit_tests: "100% pass rate"
integration_tests: "Full test suite"
security_tests: "Vulnerability scan clean"
performance_tests: "No regression"
rollback_plan:
automated: "Failed CI/CD triggers automatic rollback"
manual: "Documented rollback procedure"
monitoring: "Real-time health checks post-deployment"
```
#### Removal Process
1. **Deprecation Planning**
- Identify deprecated/unused dependencies
- Assess removal impact and effort
- Plan migration timeline and strategy
- Communicate to stakeholders
2. **Safe Removal**
```bash
# Example removal checklist
echo "Dependency Removal Checklist:"
echo "1. [ ] Grep codebase for all imports/usage"
echo "2. [ ] Check if any other dependencies require it"
echo "3. [ ] Remove from package files"
echo "4. [ ] Run full test suite"
echo "5. [ ] Update documentation"
echo "6. [ ] Deploy with monitoring"
```
### Version Management
#### Semantic Versioning Strategy
##### Version Pinning Policies
```yaml
version_pinning:
production_dependencies:
strategy: "Exact pinning"
example: "react: 18.2.0"
rationale: "Predictable builds, security control"
development_dependencies:
strategy: "Compatible range"
example: "eslint: ^8.0.0"
rationale: "Allow bug fixes and improvements"
internal_libraries:
strategy: "Compatible range"
example: "^1.2.0"
rationale: "Internal control, faster iteration"
```
##### Update Windows
- **Patch Updates (x.y.Z)**: Allow automatically with testing
- **Minor Updates (x.Y.z)**: Review monthly, apply quarterly
- **Major Updates (X.y.z)**: Annual review cycle, planned migrations
#### Lockfile Management
##### Best Practices
1. **Always Commit Lockfiles**
- package-lock.json (npm)
- yarn.lock (Yarn)
- Pipfile.lock (Python)
- Cargo.lock (Rust)
- go.sum (Go)
2. **Lockfile Validation**
```bash
# Example CI validation
- name: Validate lockfile
run: |
npm ci --audit
npm audit --audit-level moderate
# Verify lockfile is up to date
npm install --package-lock-only
git diff --exit-code package-lock.json
```
3. **Regeneration Policy**
- Regenerate monthly or after significant updates
- Always regenerate after security updates
- Document regeneration in change logs
## Security Management
### Vulnerability Management
#### Continuous Monitoring
```yaml
monitoring_stack:
scanning_tools:
- name: "Snyk"
scope: "All ecosystems"
frequency: "Daily"
integration: "CI/CD + IDE"
- name: "GitHub Dependabot"
scope: "GitHub repositories"
frequency: "Real-time"
integration: "Pull requests"
- name: "OWASP Dependency Check"
scope: "Java/.NET focus"
frequency: "Build pipeline"
integration: "CI/CD gates"
alerting:
channels: ["Slack", "Email", "PagerDuty"]
escalation:
critical: "Immediate notification"
high: "Within 1 hour"
medium: "Daily digest"
```
#### Response Procedures
##### Critical Vulnerability Response
```
Critical Vulnerability (CVSS 9.0+) Response:
│
0-2 hours: Detection & Assessment
├── Automated scan identifies vulnerability
├── Security team notified immediately
└── Initial impact assessment started
│
2-6 hours: Planning & Communication
├── Detailed impact analysis completed
├── Fix strategy determined
├── Stakeholder communication initiated
└── Emergency change approval obtained
│
6-24 hours: Implementation & Testing
├── Fix implemented in development
├── Security testing performed
├── Limited rollout to staging
└── Production deployment prepared
│
24-48 hours: Deployment & Validation
├── Production deployment executed
├── Monitoring and validation performed
├── Post-deployment testing completed
└── Incident documentation finalized
```
### Supply Chain Security
#### Source Verification
1. **Package Authenticity**
- Verify package signatures when available
- Use official package registries
- Check package maintainer reputation
- Validate download checksums
2. **Build Reproducibility**
- Use deterministic builds where possible
- Pin dependency versions exactly
- Document build environment requirements
- Maintain build artifact checksums
#### Dependency Provenance
```yaml
provenance_tracking:
metadata_collection:
- package_name: "Library identification"
- version: "Exact version used"
- source_url: "Official repository"
- maintainer: "Package maintainer info"
- license: "License verification"
- checksum: "Content verification"
verification_process:
- signature_check: "GPG signature validation"
- reputation_check: "Maintainer history review"
- content_analysis: "Static code analysis"
- behavior_monitoring: "Runtime behavior analysis"
```
## Multi-Language Considerations
### Ecosystem-Specific Practices
#### JavaScript/Node.js
```json
{
"npm_practices": {
"package_json": {
"engines": "Specify Node.js version requirements",
"dependencies": "Production dependencies only",
"devDependencies": "Development tools and testing",
"optionalDependencies": "Use sparingly, document why"
},
"security": {
"npm_audit": "Run in CI/CD pipeline",
"package_lock": "Always commit to repository",
"registry": "Use official npm registry or approved mirrors"
},
"performance": {
"bundle_analysis": "Regular bundle size monitoring",
"tree_shaking": "Ensure unused code is eliminated",
"code_splitting": "Lazy load dependencies when possible"
}
}
}
```
#### Python
```yaml
python_practices:
dependency_files:
requirements.txt: "Pin exact versions for production"
requirements-dev.txt: "Development dependencies"
setup.py: "Package distribution metadata"
pyproject.toml: "Modern Python packaging"
virtual_environments:
purpose: "Isolate project dependencies"
tools: ["venv", "virtualenv", "conda", "poetry"]
best_practice: "One environment per project"
security:
tools: ["safety", "pip-audit", "bandit"]
practices: ["Pin versions", "Use private PyPI if needed"]
```
#### Java/Maven
```xml
<!-- Maven best practices -->
<properties>
<!-- Define version properties -->
<spring.version>5.3.21</spring.version>
<junit.version>5.8.2</junit.version>
</properties>
<dependencyManagement>
<!-- Centralize version management -->
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-bom</artifactId>
<version>spring.version</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```
### Cross-Language Integration
#### API Boundaries
- Define clear service interfaces
- Use standard protocols (HTTP, gRPC)
- Document API contracts
- Version APIs independently
#### Shared Dependencies
- Minimize shared dependencies across services
- Use containerization for isolation
- Document shared dependency policies
- Monitor for version conflicts
## Performance and Optimization
### Bundle Size Management
#### Analysis Tools
```bash
# JavaScript bundle analysis
npm install -g webpack-bundle-analyzer
webpack-bundle-analyzer dist/main.js
# Python package size analysis
pip install pip-audit
pip-audit --format json | jq '.dependencies[].package_size'
# General dependency tree analysis
dep-tree analyze --format json --output deps.json
```
#### Optimization Strategies
1. **Tree Shaking**: Remove unused code
2. **Code Splitting**: Load dependencies on demand
3. **Polyfill Optimization**: Only include needed polyfills
4. **Alternative Packages**: Choose smaller alternatives when possible
### Build Performance
#### Dependency Caching
```yaml
# Example CI/CD caching
cache_strategy:
node_modules:
key: "npm-{{ checksum 'package-lock.json' }}"
paths: ["~/.npm", "node_modules"]
pip_cache:
key: "pip-{{ checksum 'requirements.txt' }}"
paths: ["~/.cache/pip"]
maven_cache:
key: "maven-{{ checksum 'pom.xml' }}"
paths: ["~/.m2/repository"]
```
#### Parallel Installation
- Configure package managers for parallel downloads
- Use local package caches
- Consider dependency proxies for enterprise environments
## Monitoring and Metrics
### Key Performance Indicators
#### Security Metrics
```yaml
security_kpis:
vulnerability_metrics:
- mean_time_to_detection: "Average time to identify vulnerabilities"
- mean_time_to_patch: "Average time to fix vulnerabilities"
- vulnerability_density: "Vulnerabilities per 1000 dependencies"
- false_positive_rate: "Percentage of false vulnerability reports"
compliance_metrics:
- license_compliance_rate: "Percentage of compliant dependencies"
- policy_violation_rate: "Rate of policy violations"
- security_gate_success_rate: "CI/CD security gate pass rate"
```
#### Operational Metrics
```yaml
operational_kpis:
maintenance_metrics:
- dependency_freshness: "Average age of dependencies"
- update_frequency: "Rate of dependency updates"
- technical_debt: "Number of outdated dependencies"
performance_metrics:
- build_time: "Time to install/build dependencies"
- bundle_size: "Final application size"
- dependency_count: "Total number of dependencies"
```
### Dashboard and Reporting
#### Executive Dashboard
- Overall risk score and trend
- Security compliance status
- Cost of dependency management
- Policy violation summary
#### Technical Dashboard
- Vulnerability count by severity
- Outdated dependency count
- Build performance metrics
- License compliance details
#### Automated Reports
- Weekly security summary
- Monthly compliance report
- Quarterly dependency review
- Annual strategy assessment
## Team Organization and Training
### Roles and Responsibilities
#### Security Champions
- Monitor security advisories
- Review dependency security scans
- Coordinate vulnerability responses
- Maintain security policies
#### Platform Engineers
- Maintain dependency management infrastructure
- Configure automated scanning and updates
- Manage package registries and mirrors
- Support development teams
#### Development Teams
- Follow dependency policies
- Perform regular security updates
- Document dependency decisions
- Participate in security training
### Training Programs
#### Security Training
- Dependency security fundamentals
- Vulnerability assessment and response
- Secure coding practices
- Supply chain attack awareness
#### Tool Training
- Package manager best practices
- Security scanning tool usage
- CI/CD security integration
- Incident response procedures
## Conclusion
Effective dependency management requires a holistic approach combining technical practices, organizational policies, and cultural awareness. Key success factors:
1. **Proactive Strategy**: Plan dependency management from project inception
2. **Clear Governance**: Establish and enforce dependency policies
3. **Automated Processes**: Use tools to scale security and maintenance
4. **Continuous Monitoring**: Stay informed about dependency risks and updates
5. **Team Training**: Ensure all team members understand security implications
6. **Regular Review**: Periodically assess and improve dependency practices
Remember that dependency management is an investment in long-term project health, security, and maintainability. The upfront effort to establish good practices pays dividends in reduced security risks, easier maintenance, and more stable software systems.
FILE:references/license_compatibility_matrix.md
# License Compatibility Matrix
This document provides a comprehensive reference for understanding license compatibility when combining open source software dependencies in your projects.
## Understanding License Types
### Permissive Licenses
- **MIT License**: Very permissive, allows commercial use, modification, and distribution
- **Apache 2.0**: Permissive with patent grant and trademark restrictions
- **BSD 3-Clause**: Permissive with non-endorsement clause
- **BSD 2-Clause**: Simple permissive license
- **ISC License**: Functionally equivalent to MIT
### Weak Copyleft Licenses
- **LGPL 2.1/3.0**: Library-level copyleft, allows linking but requires modifications to be shared
- **MPL 2.0**: File-level copyleft, compatible with many licenses
### Strong Copyleft Licenses
- **GPL 2.0/3.0**: Requires entire derivative work to be GPL-licensed
- **AGPL 3.0**: Extends GPL to network services (SaaS applications)
## Compatibility Matrix
| Project License | MIT | Apache-2.0 | BSD-3 | LGPL-2.1 | LGPL-3.0 | MPL-2.0 | GPL-2.0 | GPL-3.0 | AGPL-3.0 |
|----------------|-----|------------|-------|----------|----------|---------|---------|---------|----------|
| **MIT** | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ⚠️ | ❌ | ❌ | ❌ |
| **Apache-2.0** | ✅ | ✅ | ✅ | ❌ | ⚠️ | ✅ | ❌ | ⚠️ | ⚠️ |
| **BSD-3** | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ⚠️ | ❌ | ❌ | ❌ |
| **LGPL-2.1** | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
| **LGPL-3.0** | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ |
| **MPL-2.0** | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ |
| **GPL-2.0** | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
| **GPL-3.0** | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ |
| **AGPL-3.0** | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ |
**Legend:**
- ✅ Generally Compatible
- ⚠️ Compatible with conditions/restrictions
- ❌ Incompatible
## Detailed Compatibility Rules
### MIT Project with Other Licenses
**Compatible:**
- MIT, Apache-2.0, BSD (all variants), ISC: Full compatibility
- LGPL 2.1/3.0: Can use LGPL libraries via dynamic linking
- MPL 2.0: Can use MPL modules, must keep MPL files under MPL
**Incompatible:**
- GPL 2.0/3.0: GPL requires entire project to be GPL
- AGPL 3.0: AGPL extends to network services
### Apache 2.0 Project with Other Licenses
**Compatible:**
- MIT, BSD, ISC: Full compatibility
- LGPL 3.0: Compatible (LGPL 3.0 has Apache compatibility clause)
- MPL 2.0: Compatible
- GPL 3.0: Compatible (GPL 3.0 has Apache compatibility clause)
**Incompatible:**
- LGPL 2.1: License incompatibility
- GPL 2.0: License incompatibility (no Apache clause)
### GPL Projects
**GPL 2.0 Compatible:**
- MIT, BSD, ISC: Can incorporate permissive code
- LGPL 2.1: Compatible
- Other GPL 2.0: Compatible
**GPL 2.0 Incompatible:**
- Apache 2.0: Different patent clauses
- LGPL 3.0: Version incompatibility
- GPL 3.0: Version incompatibility
**GPL 3.0 Compatible:**
- All permissive licenses (MIT, Apache, BSD, ISC)
- LGPL 3.0: Version compatibility
- MPL 2.0: Explicit compatibility
## Common Compatibility Scenarios
### Scenario 1: Permissive Project with GPL Dependency
**Problem:** MIT-licensed project wants to use GPL library
**Impact:** Entire project must become GPL-licensed
**Solutions:**
1. Find alternative non-GPL library
2. Use dynamic linking (if possible)
3. Change project license to GPL
4. Remove the dependency
### Scenario 2: Apache Project with GPL 2.0 Dependency
**Problem:** Apache 2.0 project with GPL 2.0 dependency
**Impact:** License incompatibility due to patent clauses
**Solutions:**
1. Upgrade to GPL 3.0 if available
2. Find alternative library
3. Use via separate service (API boundary)
### Scenario 3: Commercial Product with AGPL Dependency
**Problem:** Proprietary software using AGPL library
**Impact:** AGPL copyleft extends to network services
**Solutions:**
1. Obtain commercial license
2. Replace with permissive alternative
3. Use via separate service with API boundary
4. Make entire application AGPL
## License Combination Rules
### Safe Combinations
1. **Permissive + Permissive**: Always safe
2. **Permissive + Weak Copyleft**: Usually safe with proper attribution
3. **GPL + Compatible Permissive**: Safe, result is GPL
### Risky Combinations
1. **Apache 2.0 + GPL 2.0**: Incompatible patent terms
2. **Different GPL versions**: Version compatibility issues
3. **Permissive + Strong Copyleft**: Changes project licensing
### Forbidden Combinations
1. **MIT + GPL** (without relicensing)
2. **Proprietary + Any Copyleft**
3. **LGPL 2.1 + Apache 2.0**
## Distribution Considerations
### Binary Distribution
- Must include all required license texts
- Must preserve copyright notices
- Must include source code for copyleft licenses
- Must provide installation instructions for LGPL
### Source Distribution
- Must include original license files
- Must preserve copyright headers
- Must document any modifications
- Must provide clear licensing information
### SaaS/Network Services
- AGPL extends copyleft to network services
- GPL/LGPL generally don't apply to network services
- Consider service boundaries carefully
## Compliance Best Practices
### 1. License Inventory
- Maintain complete list of all dependencies
- Track license changes in updates
- Document license obligations
### 2. Compatibility Checking
- Use automated tools for license scanning
- Implement CI/CD license gates
- Regular compliance audits
### 3. Documentation
- Clear project license declaration
- Complete attribution files
- License change history
### 4. Legal Review
- Consult legal counsel for complex scenarios
- Review before major releases
- Consider business model implications
## Risk Mitigation Strategies
### High-Risk Licenses
- **AGPL**: Avoid in commercial/proprietary projects
- **GPL in permissive projects**: Plan migration strategy
- **Unknown licenses**: Investigate immediately
### Medium-Risk Scenarios
- **Version incompatibilities**: Upgrade when possible
- **Patent clause conflicts**: Seek legal advice
- **Multiple copyleft licenses**: Verify compatibility
### Risk Assessment Framework
1. **Identify** all dependencies and their licenses
2. **Classify** by license type and risk level
3. **Analyze** compatibility with project license
4. **Document** decisions and rationale
5. **Monitor** for license changes
## Common Misconceptions
### ❌ Wrong Assumptions
- "MIT allows everything" (still requires attribution)
- "Linking doesn't create derivatives" (depends on license)
- "GPL only affects distribution" (AGPL affects network use)
- "Commercial use is always forbidden" (most FOSS allows it)
### ✅ Correct Understanding
- Each license has specific requirements
- Combination creates most restrictive terms
- Network use may trigger copyleft (AGPL)
- Commercial licensing options often available
## Quick Reference Decision Tree
```
Is the dependency GPL/AGPL?
├─ YES → Is your project commercial/proprietary?
│ ├─ YES → ❌ Incompatible (find alternative)
│ └─ NO → ✅ Compatible (if same GPL version)
└─ NO → Is it permissive (MIT/Apache/BSD)?
├─ YES → ✅ Generally compatible
└─ NO → Check specific compatibility matrix
```
## Tools and Resources
### Automated Tools
- **FOSSA**: Commercial license scanning
- **WhiteSource**: Enterprise license management
- **ORT**: Open source license scanning
- **License Finder**: Ruby-based license detection
### Manual Review Resources
- **choosealicense.com**: License picker and comparison
- **SPDX License List**: Standardized license identifiers
- **FSF License List**: Free Software Foundation compatibility
- **OSI Approved Licenses**: Open Source Initiative approved licenses
## Conclusion
License compatibility is crucial for legal compliance and risk management. When in doubt:
1. **Choose permissive licenses** for maximum compatibility
2. **Avoid strong copyleft** in proprietary projects
3. **Document all license decisions** thoroughly
4. **Consult legal experts** for complex scenarios
5. **Use automated tools** for continuous monitoring
Remember: This matrix provides general guidance but legal requirements may vary by jurisdiction and specific use cases. Always consult with legal counsel for important licensing decisions.
FILE:references/vulnerability_assessment_guide.md
# Vulnerability Assessment Guide
A comprehensive guide to assessing, prioritizing, and managing security vulnerabilities in software dependencies.
## Overview
Dependency vulnerabilities represent one of the most significant attack vectors in modern software systems. This guide provides a structured approach to vulnerability assessment, risk scoring, and remediation planning.
## Vulnerability Classification System
### Severity Levels (CVSS 3.1)
#### Critical (9.0 - 10.0)
- **Impact**: Complete system compromise possible
- **Examples**: Remote code execution, privilege escalation to admin
- **Response Time**: Immediate (within 24 hours)
- **Business Risk**: System shutdown, data breach, regulatory violations
#### High (7.0 - 8.9)
- **Impact**: Significant security impact
- **Examples**: SQL injection, authentication bypass, sensitive data exposure
- **Response Time**: 7 days maximum
- **Business Risk**: Data compromise, service disruption
#### Medium (4.0 - 6.9)
- **Impact**: Moderate security impact
- **Examples**: Cross-site scripting (XSS), information disclosure
- **Response Time**: 30 days
- **Business Risk**: Limited data exposure, minor service impact
#### Low (0.1 - 3.9)
- **Impact**: Limited security impact
- **Examples**: Denial of service (limited), minor information leakage
- **Response Time**: Next planned release cycle
- **Business Risk**: Minimal impact on operations
## Vulnerability Types and Patterns
### Code Injection Vulnerabilities
#### SQL Injection
- **CWE-89**: Improper neutralization of SQL commands
- **Common in**: Database interaction libraries, ORM frameworks
- **Detection**: Parameter handling analysis, query construction review
- **Mitigation**: Parameterized queries, input validation, least privilege DB access
#### Command Injection
- **CWE-78**: OS command injection
- **Common in**: System utilities, file processing libraries
- **Detection**: System call analysis, user input handling
- **Mitigation**: Input sanitization, avoid system calls, sandboxing
#### Code Injection
- **CWE-94**: Code injection
- **Common in**: Template engines, dynamic code evaluation
- **Detection**: eval() usage, dynamic code generation
- **Mitigation**: Avoid dynamic code execution, input validation, sandboxing
### Authentication and Authorization
#### Authentication Bypass
- **CWE-287**: Improper authentication
- **Common in**: Authentication libraries, session management
- **Detection**: Authentication flow analysis, session handling review
- **Mitigation**: Multi-factor authentication, secure session management
#### Privilege Escalation
- **CWE-269**: Improper privilege management
- **Common in**: Authorization frameworks, access control libraries
- **Detection**: Permission checking analysis, role validation
- **Mitigation**: Principle of least privilege, proper access controls
### Data Exposure
#### Sensitive Data Exposure
- **CWE-200**: Information exposure
- **Common in**: Logging libraries, error handling, API responses
- **Detection**: Log output analysis, error message review
- **Mitigation**: Data classification, sanitized logging, proper error handling
#### Cryptographic Failures
- **CWE-327**: Broken cryptography
- **Common in**: Cryptographic libraries, hash functions
- **Detection**: Algorithm analysis, key management review
- **Mitigation**: Modern cryptographic standards, proper key management
### Input Validation Issues
#### Cross-Site Scripting (XSS)
- **CWE-79**: Improper neutralization of input
- **Common in**: Web frameworks, template engines
- **Detection**: Input handling analysis, output encoding review
- **Mitigation**: Input validation, output encoding, Content Security Policy
#### Deserialization Vulnerabilities
- **CWE-502**: Deserialization of untrusted data
- **Common in**: Serialization libraries, data processing
- **Detection**: Deserialization usage analysis
- **Mitigation**: Avoid untrusted deserialization, input validation
## Risk Assessment Framework
### CVSS Scoring Components
#### Base Metrics
1. **Attack Vector (AV)**
- Network (N): 0.85
- Adjacent (A): 0.62
- Local (L): 0.55
- Physical (P): 0.2
2. **Attack Complexity (AC)**
- Low (L): 0.77
- High (H): 0.44
3. **Privileges Required (PR)**
- None (N): 0.85
- Low (L): 0.62/0.68
- High (H): 0.27/0.50
4. **User Interaction (UI)**
- None (N): 0.85
- Required (R): 0.62
5. **Impact Metrics (C/I/A)**
- High (H): 0.56
- Low (L): 0.22
- None (N): 0
#### Temporal Metrics
- **Exploit Code Maturity**: Proof of concept availability
- **Remediation Level**: Official fix availability
- **Report Confidence**: Vulnerability confirmation level
#### Environmental Metrics
- **Confidentiality/Integrity/Availability Requirements**: Business impact
- **Modified Base Metrics**: Environment-specific adjustments
### Custom Risk Factors
#### Business Context
1. **Data Sensitivity**
- Public data: Low risk multiplier (1.0x)
- Internal data: Medium risk multiplier (1.2x)
- Customer data: High risk multiplier (1.5x)
- Regulated data: Critical risk multiplier (2.0x)
2. **System Criticality**
- Development: Low impact (1.0x)
- Staging: Medium impact (1.3x)
- Production: High impact (1.8x)
- Core infrastructure: Critical impact (2.5x)
3. **Exposure Level**
- Internal systems: Base risk
- Partner access: +1 risk level
- Public internet: +2 risk levels
- High-value target: +3 risk levels
#### Technical Factors
1. **Dependency Type**
- Direct dependencies: Higher priority
- Transitive dependencies: Lower priority (unless critical path)
- Development dependencies: Lowest priority
2. **Usage Pattern**
- Core functionality: Highest priority
- Optional features: Medium priority
- Unused code paths: Lowest priority
3. **Fix Availability**
- Official patch available: Standard timeline
- Workaround available: Extended timeline acceptable
- No fix available: Risk acceptance or replacement needed
## Vulnerability Discovery and Monitoring
### Automated Scanning
#### Dependency Scanners
- **npm audit**: Node.js ecosystem
- **pip-audit**: Python ecosystem
- **bundler-audit**: Ruby ecosystem
- **OWASP Dependency Check**: Multi-language support
#### Continuous Monitoring
```bash
# Example CI/CD integration
name: Security Scan
on: [push, pull_request, schedule]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run dependency audit
run: |
npm audit --audit-level high
python -m pip_audit
bundle audit
```
#### Commercial Tools
- **Snyk**: Developer-first security platform
- **WhiteSource**: Enterprise dependency management
- **Veracode**: Application security platform
- **Checkmarx**: Static application security testing
### Manual Assessment
#### Code Review Checklist
1. **Input Validation**
- [ ] All user inputs validated
- [ ] Proper sanitization applied
- [ ] Length and format restrictions
2. **Authentication/Authorization**
- [ ] Proper authentication checks
- [ ] Authorization at every access point
- [ ] Session management secure
3. **Data Handling**
- [ ] Sensitive data protected
- [ ] Encryption properly implemented
- [ ] Secure data transmission
4. **Error Handling**
- [ ] No sensitive info in error messages
- [ ] Proper logging without data leaks
- [ ] Graceful error handling
## Prioritization Framework
### Priority Matrix
| Severity | Exploitability | Business Impact | Priority Level |
|----------|---------------|-----------------|---------------|
| Critical | High | High | P0 (Immediate) |
| Critical | High | Medium | P0 (Immediate) |
| Critical | Medium | High | P1 (24 hours) |
| High | High | High | P1 (24 hours) |
| High | High | Medium | P2 (1 week) |
| High | Medium | High | P2 (1 week) |
| Medium | High | High | P2 (1 week) |
| All Others | - | - | P3 (30 days) |
### Prioritization Factors
#### Technical Factors (40% weight)
1. **CVSS Base Score** (15%)
2. **Exploit Availability** (10%)
3. **Fix Complexity** (8%)
4. **Dependency Criticality** (7%)
#### Business Factors (35% weight)
1. **Data Impact** (15%)
2. **System Criticality** (10%)
3. **Regulatory Requirements** (5%)
4. **Customer Impact** (5%)
#### Operational Factors (25% weight)
1. **Attack Surface** (10%)
2. **Monitoring Coverage** (8%)
3. **Incident Response Capability** (7%)
### Scoring Formula
```
Priority Score = (Technical Score × 0.4) + (Business Score × 0.35) + (Operational Score × 0.25)
Where each component is scored 1-10:
- 9-10: Critical priority
- 7-8: High priority
- 5-6: Medium priority
- 3-4: Low priority
- 1-2: Informational
```
## Remediation Strategies
### Immediate Actions (P0/P1)
#### Hot Fixes
1. **Version Upgrade**
- Update to patched version
- Test critical functionality
- Deploy with rollback plan
2. **Configuration Changes**
- Disable vulnerable features
- Implement additional access controls
- Add monitoring/alerting
3. **Workarounds**
- Input validation layers
- Network-level protections
- Application-level mitigations
#### Emergency Response Process
```
1. Vulnerability Confirmed
↓
2. Impact Assessment (2 hours)
↓
3. Mitigation Strategy (4 hours)
↓
4. Implementation & Testing (12 hours)
↓
5. Deployment (2 hours)
↓
6. Monitoring & Validation (ongoing)
```
### Planned Remediation (P2/P3)
#### Standard Update Process
1. **Assessment Phase**
- Detailed impact analysis
- Testing requirements
- Rollback procedures
2. **Planning Phase**
- Update scheduling
- Resource allocation
- Communication plan
3. **Implementation Phase**
- Development environment testing
- Staging environment validation
- Production deployment
4. **Validation Phase**
- Functionality verification
- Security testing
- Performance monitoring
### Alternative Approaches
#### Dependency Replacement
- **When to Consider**: No fix available, persistent vulnerabilities
- **Process**: Impact analysis → Alternative evaluation → Migration planning
- **Risks**: API changes, feature differences, stability concerns
#### Accept Risk (Last Resort)
- **Criteria**: Very low probability, minimal impact, no feasible fix
- **Requirements**: Executive approval, documented risk acceptance, monitoring
- **Conditions**: Regular re-assessment, alternative solution tracking
## Remediation Tracking
### Metrics and KPIs
#### Vulnerability Metrics
- **Mean Time to Detection (MTTD)**: Average time from publication to discovery
- **Mean Time to Patch (MTTP)**: Average time from discovery to fix deployment
- **Vulnerability Density**: Vulnerabilities per 1000 dependencies
- **Fix Rate**: Percentage of vulnerabilities fixed within SLA
#### Trend Analysis
- **Monthly vulnerability counts by severity**
- **Average age of unpatched vulnerabilities**
- **Remediation timeline trends**
- **False positive rates**
#### Reporting Dashboard
```
Security Dashboard Components:
├── Current Vulnerability Status
│ ├── Critical: 2 (SLA: 24h)
│ ├── High: 5 (SLA: 7d)
│ └── Medium: 12 (SLA: 30d)
├── Trend Analysis
│ ├── New vulnerabilities (last 30 days)
│ ├── Fixed vulnerabilities (last 30 days)
│ └── Average resolution time
└── Risk Assessment
├── Overall risk score
├── Top vulnerable components
└── Compliance status
```
## Documentation Requirements
### Vulnerability Records
Each vulnerability should be documented with:
- **CVE/Advisory ID**: Official vulnerability identifier
- **Discovery Date**: When vulnerability was identified
- **CVSS Score**: Base and environmental scores
- **Affected Systems**: Components and versions impacted
- **Business Impact**: Risk assessment and criticality
- **Remediation Plan**: Planned fix approach and timeline
- **Resolution Date**: When fix was implemented and verified
### Risk Acceptance Documentation
For accepted risks, document:
- **Risk Description**: Detailed vulnerability explanation
- **Impact Analysis**: Potential business and technical impact
- **Mitigation Measures**: Compensating controls implemented
- **Acceptance Rationale**: Why risk is being accepted
- **Review Schedule**: When risk will be reassessed
- **Approver**: Who authorized the risk acceptance
## Integration with Development Workflow
### Shift-Left Security
#### Development Phase
- **IDE Integration**: Real-time vulnerability detection
- **Pre-commit Hooks**: Automated security checks
- **Code Review**: Security-focused review criteria
#### CI/CD Integration
- **Build Stage**: Dependency vulnerability scanning
- **Test Stage**: Security test automation
- **Deploy Stage**: Final security validation
#### Production Monitoring
- **Runtime Protection**: Web application firewalls, runtime security
- **Continuous Scanning**: Regular dependency updates check
- **Incident Response**: Automated vulnerability alert handling
### Security Gates
```yaml
security_gates:
development:
- dependency_scan: true
- secret_detection: true
- code_quality: true
staging:
- penetration_test: true
- compliance_check: true
- performance_test: true
production:
- final_security_scan: true
- change_approval: required
- rollback_plan: verified
```
## Best Practices Summary
### Proactive Measures
1. **Regular Scanning**: Automated daily/weekly scans
2. **Update Schedule**: Regular dependency maintenance
3. **Security Training**: Developer security awareness
4. **Threat Modeling**: Understanding attack vectors
### Reactive Measures
1. **Incident Response**: Well-defined process for critical vulnerabilities
2. **Communication Plan**: Stakeholder notification procedures
3. **Lessons Learned**: Post-incident analysis and improvement
4. **Recovery Procedures**: Rollback and recovery capabilities
### Organizational Considerations
1. **Responsibility Assignment**: Clear ownership of security tasks
2. **Resource Allocation**: Adequate security budget and staffing
3. **Tool Selection**: Appropriate security tools for organization size
4. **Compliance Requirements**: Meeting regulatory and industry standards
Remember: Vulnerability management is an ongoing process requiring continuous attention, regular updates to procedures, and organizational commitment to security best practices.
FILE:scripts/dep_scanner.py
#!/usr/bin/env python3
"""
Dependency Scanner - Multi-language dependency vulnerability and analysis tool.
This script parses dependency files from various package managers, extracts direct
and transitive dependencies, checks against built-in vulnerability databases,
and provides comprehensive security analysis with actionable recommendations.
Author: Claude Skills Engineering Team
License: MIT
"""
import json
import os
import re
import sys
import argparse
from typing import Dict, List, Set, Any, Optional, Tuple
from pathlib import Path
from dataclasses import dataclass, asdict
from datetime import datetime
import hashlib
import subprocess
@dataclass
class Vulnerability:
"""Represents a security vulnerability."""
id: str
summary: str
severity: str
cvss_score: float
affected_versions: str
fixed_version: Optional[str]
published_date: str
references: List[str]
@dataclass
class Dependency:
"""Represents a project dependency."""
name: str
version: str
ecosystem: str
direct: bool
license: Optional[str] = None
description: Optional[str] = None
homepage: Optional[str] = None
vulnerabilities: List[Vulnerability] = None
def __post_init__(self):
if self.vulnerabilities is None:
self.vulnerabilities = []
class DependencyScanner:
"""Main dependency scanner class."""
def __init__(self):
self.known_vulnerabilities = self._load_vulnerability_database()
self.supported_files = {
'package.json': self._parse_package_json,
'package-lock.json': self._parse_package_lock,
'yarn.lock': self._parse_yarn_lock,
'requirements.txt': self._parse_requirements_txt,
'pyproject.toml': self._parse_pyproject_toml,
'Pipfile.lock': self._parse_pipfile_lock,
'poetry.lock': self._parse_poetry_lock,
'go.mod': self._parse_go_mod,
'go.sum': self._parse_go_sum,
'Cargo.toml': self._parse_cargo_toml,
'Cargo.lock': self._parse_cargo_lock,
'Gemfile': self._parse_gemfile,
'Gemfile.lock': self._parse_gemfile_lock,
}
def _load_vulnerability_database(self) -> Dict[str, List[Vulnerability]]:
"""Load built-in vulnerability database with common CVE patterns."""
return {
# JavaScript/Node.js vulnerabilities
'lodash': [
Vulnerability(
id='CVE-2021-23337',
summary='Prototype pollution in lodash',
severity='HIGH',
cvss_score=7.2,
affected_versions='<4.17.21',
fixed_version='4.17.21',
published_date='2021-02-15',
references=['https://nvd.nist.gov/vuln/detail/CVE-2021-23337']
)
],
'axios': [
Vulnerability(
id='CVE-2023-45857',
summary='Cross-site request forgery in axios',
severity='MEDIUM',
cvss_score=6.1,
affected_versions='>=1.0.0 <1.6.0',
fixed_version='1.6.0',
published_date='2023-10-11',
references=['https://nvd.nist.gov/vuln/detail/CVE-2023-45857']
)
],
'express': [
Vulnerability(
id='CVE-2022-24999',
summary='Open redirect in express',
severity='MEDIUM',
cvss_score=6.1,
affected_versions='<4.18.2',
fixed_version='4.18.2',
published_date='2022-11-26',
references=['https://nvd.nist.gov/vuln/detail/CVE-2022-24999']
)
],
# Python vulnerabilities
'django': [
Vulnerability(
id='CVE-2024-27351',
summary='SQL injection in Django',
severity='HIGH',
cvss_score=9.8,
affected_versions='>=3.2 <4.2.11',
fixed_version='4.2.11',
published_date='2024-02-06',
references=['https://nvd.nist.gov/vuln/detail/CVE-2024-27351']
)
],
'requests': [
Vulnerability(
id='CVE-2023-32681',
summary='Proxy-authorization header leak in requests',
severity='MEDIUM',
cvss_score=6.1,
affected_versions='>=2.3.0 <2.31.0',
fixed_version='2.31.0',
published_date='2023-05-26',
references=['https://nvd.nist.gov/vuln/detail/CVE-2023-32681']
)
],
'pillow': [
Vulnerability(
id='CVE-2023-50447',
summary='Arbitrary code execution in Pillow',
severity='HIGH',
cvss_score=8.8,
affected_versions='<10.2.0',
fixed_version='10.2.0',
published_date='2024-01-02',
references=['https://nvd.nist.gov/vuln/detail/CVE-2023-50447']
)
],
# Go vulnerabilities
'github.com/gin-gonic/gin': [
Vulnerability(
id='CVE-2023-26125',
summary='Path traversal in gin',
severity='HIGH',
cvss_score=7.5,
affected_versions='<1.9.1',
fixed_version='1.9.1',
published_date='2023-02-28',
references=['https://nvd.nist.gov/vuln/detail/CVE-2023-26125']
)
],
# Rust vulnerabilities
'serde': [
Vulnerability(
id='RUSTSEC-2022-0061',
summary='Deserialization vulnerability in serde',
severity='HIGH',
cvss_score=8.2,
affected_versions='<1.0.152',
fixed_version='1.0.152',
published_date='2022-12-07',
references=['https://rustsec.org/advisories/RUSTSEC-2022-0061']
)
],
# Ruby vulnerabilities
'rails': [
Vulnerability(
id='CVE-2023-28362',
summary='ReDoS vulnerability in Rails',
severity='HIGH',
cvss_score=7.5,
affected_versions='>=7.0.0 <7.0.4.3',
fixed_version='7.0.4.3',
published_date='2023-03-13',
references=['https://nvd.nist.gov/vuln/detail/CVE-2023-28362']
)
]
}
def scan_project(self, project_path: str) -> Dict[str, Any]:
"""Scan a project directory for dependencies and vulnerabilities."""
project_path = Path(project_path)
if not project_path.exists():
raise FileNotFoundError(f"Project path does not exist: {project_path}")
scan_results = {
'timestamp': datetime.now().isoformat(),
'project_path': str(project_path),
'dependencies': [],
'vulnerabilities_found': 0,
'high_severity_count': 0,
'medium_severity_count': 0,
'low_severity_count': 0,
'ecosystems': set(),
'scan_summary': {},
'recommendations': []
}
# Find and parse dependency files
for file_pattern, parser in self.supported_files.items():
matching_files = list(project_path.rglob(file_pattern))
for dep_file in matching_files:
try:
dependencies = parser(dep_file)
scan_results['dependencies'].extend(dependencies)
for dep in dependencies:
scan_results['ecosystems'].add(dep.ecosystem)
# Check for vulnerabilities
vulnerabilities = self._check_vulnerabilities(dep)
dep.vulnerabilities = vulnerabilities
scan_results['vulnerabilities_found'] += len(vulnerabilities)
for vuln in vulnerabilities:
if vuln.severity == 'HIGH':
scan_results['high_severity_count'] += 1
elif vuln.severity == 'MEDIUM':
scan_results['medium_severity_count'] += 1
else:
scan_results['low_severity_count'] += 1
except Exception as e:
print(f"Error parsing {dep_file}: {e}")
continue
scan_results['ecosystems'] = list(scan_results['ecosystems'])
scan_results['scan_summary'] = self._generate_scan_summary(scan_results)
scan_results['recommendations'] = self._generate_recommendations(scan_results)
return scan_results
def _check_vulnerabilities(self, dependency: Dependency) -> List[Vulnerability]:
"""Check if a dependency has known vulnerabilities."""
vulnerabilities = []
# Check package name (exact match and common variations)
package_names = [dependency.name, dependency.name.lower()]
for pkg_name in package_names:
if pkg_name in self.known_vulnerabilities:
for vuln in self.known_vulnerabilities[pkg_name]:
if self._version_matches_vulnerability(dependency.version, vuln.affected_versions):
vulnerabilities.append(vuln)
return vulnerabilities
def _version_matches_vulnerability(self, version: str, affected_pattern: str) -> bool:
"""Check if a version matches a vulnerability pattern."""
# Simple version matching - in production, use proper semver library
try:
# Handle common patterns like "<4.17.21", ">=1.0.0 <1.6.0"
if '<' in affected_pattern and '>' not in affected_pattern:
# Pattern like "<4.17.21"
max_version = affected_pattern.replace('<', '').strip()
return self._compare_versions(version, max_version) < 0
elif '>=' in affected_pattern and '<' in affected_pattern:
# Pattern like ">=1.0.0 <1.6.0"
parts = affected_pattern.split('<')
min_part = parts[0].replace('>=', '').strip()
max_part = parts[1].strip()
return (self._compare_versions(version, min_part) >= 0 and
self._compare_versions(version, max_part) < 0)
except:
pass
return False
def _compare_versions(self, v1: str, v2: str) -> int:
"""Simple version comparison. Returns -1, 0, or 1."""
try:
def normalize(v):
return [int(x) for x in re.sub(r'(\.0+)*$','', v).split('.')]
v1_parts = normalize(v1)
v2_parts = normalize(v2)
if v1_parts < v2_parts:
return -1
elif v1_parts > v2_parts:
return 1
else:
return 0
except:
return 0
# Package file parsers
def _parse_package_json(self, file_path: Path) -> List[Dependency]:
"""Parse package.json for Node.js dependencies."""
dependencies = []
try:
with open(file_path, 'r') as f:
data = json.load(f)
# Parse dependencies
for dep_type in ['dependencies', 'devDependencies']:
if dep_type in data:
for name, version in data[dep_type].items():
dep = Dependency(
name=name,
version=version.replace('^', '').replace('~', '').replace('>=', '').replace('<=', ''),
ecosystem='npm',
direct=True
)
dependencies.append(dep)
except Exception as e:
print(f"Error parsing package.json: {e}")
return dependencies
def _parse_package_lock(self, file_path: Path) -> List[Dependency]:
"""Parse package-lock.json for Node.js transitive dependencies."""
dependencies = []
try:
with open(file_path, 'r') as f:
data = json.load(f)
if 'packages' in data:
for path, pkg_info in data['packages'].items():
if path == '': # Skip root package
continue
name = path.split('/')[-1] if '/' in path else path
version = pkg_info.get('version', '')
dep = Dependency(
name=name,
version=version,
ecosystem='npm',
direct=False,
description=pkg_info.get('description', '')
)
dependencies.append(dep)
except Exception as e:
print(f"Error parsing package-lock.json: {e}")
return dependencies
def _parse_yarn_lock(self, file_path: Path) -> List[Dependency]:
"""Parse yarn.lock for Node.js dependencies."""
dependencies = []
try:
with open(file_path, 'r') as f:
content = f.read()
# Simple yarn.lock parsing
packages = re.findall(r'^([^#\s][^:]+):\s*\n(?:\s+.*\n)*?\s+version\s+"([^"]+)"', content, re.MULTILINE)
for package_spec, version in packages:
name = package_spec.split('@')[0] if '@' in package_spec else package_spec
name = name.strip('"')
dep = Dependency(
name=name,
version=version,
ecosystem='npm',
direct=False
)
dependencies.append(dep)
except Exception as e:
print(f"Error parsing yarn.lock: {e}")
return dependencies
def _parse_requirements_txt(self, file_path: Path) -> List[Dependency]:
"""Parse requirements.txt for Python dependencies."""
dependencies = []
try:
with open(file_path, 'r') as f:
lines = f.readlines()
for line in lines:
line = line.strip()
if line and not line.startswith('#') and not line.startswith('-'):
# Parse package==version or package>=version patterns
match = re.match(r'^([a-zA-Z0-9_-]+)([><=!]+)(.+)$', line)
if match:
name, operator, version = match.groups()
dep = Dependency(
name=name,
version=version,
ecosystem='pypi',
direct=True
)
dependencies.append(dep)
except Exception as e:
print(f"Error parsing requirements.txt: {e}")
return dependencies
def _parse_pyproject_toml(self, file_path: Path) -> List[Dependency]:
"""Parse pyproject.toml for Python dependencies."""
dependencies = []
try:
with open(file_path, 'r') as f:
content = f.read()
# Simple TOML parsing for dependencies
dep_section = re.search(r'\[tool\.poetry\.dependencies\](.*?)(?=\[|\Z)', content, re.DOTALL)
if dep_section:
for line in dep_section.group(1).split('\n'):
match = re.match(r'^([a-zA-Z0-9_-]+)\s*=\s*["\']([^"\']+)["\']', line.strip())
if match:
name, version = match.groups()
if name != 'python':
dep = Dependency(
name=name,
version=version.replace('^', '').replace('~', ''),
ecosystem='pypi',
direct=True
)
dependencies.append(dep)
except Exception as e:
print(f"Error parsing pyproject.toml: {e}")
return dependencies
def _parse_pipfile_lock(self, file_path: Path) -> List[Dependency]:
"""Parse Pipfile.lock for Python dependencies."""
dependencies = []
try:
with open(file_path, 'r') as f:
data = json.load(f)
for section in ['default', 'develop']:
if section in data:
for name, info in data[section].items():
version = info.get('version', '').replace('==', '')
dep = Dependency(
name=name,
version=version,
ecosystem='pypi',
direct=(section == 'default')
)
dependencies.append(dep)
except Exception as e:
print(f"Error parsing Pipfile.lock: {e}")
return dependencies
def _parse_poetry_lock(self, file_path: Path) -> List[Dependency]:
"""Parse poetry.lock for Python dependencies."""
dependencies = []
try:
with open(file_path, 'r') as f:
content = f.read()
# Extract package entries from TOML
packages = re.findall(r'\[\[package\]\]\nname\s*=\s*"([^"]+)"\nversion\s*=\s*"([^"]+)"', content)
for name, version in packages:
dep = Dependency(
name=name,
version=version,
ecosystem='pypi',
direct=False
)
dependencies.append(dep)
except Exception as e:
print(f"Error parsing poetry.lock: {e}")
return dependencies
def _parse_go_mod(self, file_path: Path) -> List[Dependency]:
"""Parse go.mod for Go dependencies."""
dependencies = []
try:
with open(file_path, 'r') as f:
content = f.read()
# Parse require block
require_match = re.search(r'require\s*\((.*?)\)', content, re.DOTALL)
if require_match:
requires = require_match.group(1)
for line in requires.split('\n'):
match = re.match(r'\s*([^\s]+)\s+v?([^\s]+)', line.strip())
if match:
name, version = match.groups()
dep = Dependency(
name=name,
version=version,
ecosystem='go',
direct=True
)
dependencies.append(dep)
except Exception as e:
print(f"Error parsing go.mod: {e}")
return dependencies
def _parse_go_sum(self, file_path: Path) -> List[Dependency]:
"""Parse go.sum for Go dependency checksums."""
return [] # go.sum mainly contains checksums, dependencies are in go.mod
def _parse_cargo_toml(self, file_path: Path) -> List[Dependency]:
"""Parse Cargo.toml for Rust dependencies."""
dependencies = []
try:
with open(file_path, 'r') as f:
content = f.read()
# Parse [dependencies] section
dep_section = re.search(r'\[dependencies\](.*?)(?=\[|\Z)', content, re.DOTALL)
if dep_section:
for line in dep_section.group(1).split('\n'):
match = re.match(r'^([a-zA-Z0-9_-]+)\s*=\s*["\']([^"\']+)["\']', line.strip())
if match:
name, version = match.groups()
dep = Dependency(
name=name,
version=version,
ecosystem='cargo',
direct=True
)
dependencies.append(dep)
except Exception as e:
print(f"Error parsing Cargo.toml: {e}")
return dependencies
def _parse_cargo_lock(self, file_path: Path) -> List[Dependency]:
"""Parse Cargo.lock for Rust dependencies."""
dependencies = []
try:
with open(file_path, 'r') as f:
content = f.read()
# Parse [[package]] entries
packages = re.findall(r'\[\[package\]\]\nname\s*=\s*"([^"]+)"\nversion\s*=\s*"([^"]+)"', content)
for name, version in packages:
dep = Dependency(
name=name,
version=version,
ecosystem='cargo',
direct=False
)
dependencies.append(dep)
except Exception as e:
print(f"Error parsing Cargo.lock: {e}")
return dependencies
def _parse_gemfile(self, file_path: Path) -> List[Dependency]:
"""Parse Gemfile for Ruby dependencies."""
dependencies = []
try:
with open(file_path, 'r') as f:
content = f.read()
# Parse gem declarations
gems = re.findall(r'gem\s+["\']([^"\']+)["\'](?:\s*,\s*["\']([^"\']+)["\'])?', content)
for gem_info in gems:
name = gem_info[0]
version = gem_info[1] if len(gem_info) > 1 and gem_info[1] else ''
dep = Dependency(
name=name,
version=version,
ecosystem='rubygems',
direct=True
)
dependencies.append(dep)
except Exception as e:
print(f"Error parsing Gemfile: {e}")
return dependencies
def _parse_gemfile_lock(self, file_path: Path) -> List[Dependency]:
"""Parse Gemfile.lock for Ruby dependencies."""
dependencies = []
try:
with open(file_path, 'r') as f:
content = f.read()
# Extract GEM section
gem_section = re.search(r'GEM\s*\n(.*?)(?=\n\S|\Z)', content, re.DOTALL)
if gem_section:
specs = gem_section.group(1)
gems = re.findall(r'\s+([a-zA-Z0-9_-]+)\s+\(([^)]+)\)', specs)
for name, version in gems:
dep = Dependency(
name=name,
version=version,
ecosystem='rubygems',
direct=False
)
dependencies.append(dep)
except Exception as e:
print(f"Error parsing Gemfile.lock: {e}")
return dependencies
def _generate_scan_summary(self, scan_results: Dict[str, Any]) -> Dict[str, Any]:
"""Generate a summary of the scan results."""
total_deps = len(scan_results['dependencies'])
unique_deps = len(set(dep.name for dep in scan_results['dependencies']))
return {
'total_dependencies': total_deps,
'unique_dependencies': unique_deps,
'ecosystems_found': len(scan_results['ecosystems']),
'vulnerable_dependencies': len([dep for dep in scan_results['dependencies'] if dep.vulnerabilities]),
'vulnerability_breakdown': {
'high': scan_results['high_severity_count'],
'medium': scan_results['medium_severity_count'],
'low': scan_results['low_severity_count']
}
}
def _generate_recommendations(self, scan_results: Dict[str, Any]) -> List[str]:
"""Generate actionable recommendations based on scan results."""
recommendations = []
high_count = scan_results['high_severity_count']
medium_count = scan_results['medium_severity_count']
if high_count > 0:
recommendations.append(f"URGENT: Address {high_count} high-severity vulnerabilities immediately")
if medium_count > 0:
recommendations.append(f"Schedule fixes for {medium_count} medium-severity vulnerabilities within 30 days")
vulnerable_deps = [dep for dep in scan_results['dependencies'] if dep.vulnerabilities]
if vulnerable_deps:
for dep in vulnerable_deps[:3]: # Top 3 most critical
for vuln in dep.vulnerabilities:
if vuln.fixed_version:
recommendations.append(f"Update {dep.name} from {dep.version} to {vuln.fixed_version} to fix {vuln.id}")
if len(scan_results['ecosystems']) > 3:
recommendations.append("Consider consolidating package managers to reduce complexity")
return recommendations
def generate_report(self, scan_results: Dict[str, Any], format: str = 'text') -> str:
"""Generate a human-readable or JSON report."""
if format == 'json':
# Convert Dependency objects to dicts for JSON serialization
serializable_results = scan_results.copy()
serializable_results['dependencies'] = [
{
'name': dep.name,
'version': dep.version,
'ecosystem': dep.ecosystem,
'direct': dep.direct,
'license': dep.license,
'vulnerabilities': [asdict(vuln) for vuln in dep.vulnerabilities]
}
for dep in scan_results['dependencies']
]
return json.dumps(serializable_results, indent=2, default=str)
# Text format report
report = []
report.append("=" * 60)
report.append("DEPENDENCY SECURITY SCAN REPORT")
report.append("=" * 60)
report.append(f"Scan Date: {scan_results['timestamp']}")
report.append(f"Project: {scan_results['project_path']}")
report.append("")
# Summary
summary = scan_results['scan_summary']
report.append("SUMMARY:")
report.append(f" Total Dependencies: {summary['total_dependencies']}")
report.append(f" Unique Dependencies: {summary['unique_dependencies']}")
report.append(f" Ecosystems: {', '.join(scan_results['ecosystems'])}")
report.append(f" Vulnerabilities Found: {scan_results['vulnerabilities_found']}")
report.append(f" High Severity: {summary['vulnerability_breakdown']['high']}")
report.append(f" Medium Severity: {summary['vulnerability_breakdown']['medium']}")
report.append(f" Low Severity: {summary['vulnerability_breakdown']['low']}")
report.append("")
# Vulnerable dependencies
vulnerable_deps = [dep for dep in scan_results['dependencies'] if dep.vulnerabilities]
if vulnerable_deps:
report.append("VULNERABLE DEPENDENCIES:")
report.append("-" * 30)
for dep in vulnerable_deps:
report.append(f"Package: {dep.name} v{dep.version} ({dep.ecosystem})")
for vuln in dep.vulnerabilities:
report.append(f" • {vuln.id}: {vuln.summary}")
report.append(f" Severity: {vuln.severity} (CVSS: {vuln.cvss_score})")
if vuln.fixed_version:
report.append(f" Fixed in: {vuln.fixed_version}")
report.append("")
# Recommendations
if scan_results['recommendations']:
report.append("RECOMMENDATIONS:")
report.append("-" * 20)
for i, rec in enumerate(scan_results['recommendations'], 1):
report.append(f"{i}. {rec}")
report.append("")
report.append("=" * 60)
return '\n'.join(report)
def main():
"""Main entry point for the dependency scanner."""
parser = argparse.ArgumentParser(
description='Scan project dependencies for vulnerabilities and security issues',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python dep_scanner.py /path/to/project
python dep_scanner.py . --format json --output results.json
python dep_scanner.py /app --fail-on-high
"""
)
parser.add_argument('project_path',
help='Path to the project directory to scan')
parser.add_argument('--format', choices=['text', 'json'], default='text',
help='Output format (default: text)')
parser.add_argument('--output', '-o',
help='Output file path (default: stdout)')
parser.add_argument('--fail-on-high', action='store_true',
help='Exit with error code if high-severity vulnerabilities found')
parser.add_argument('--quick-scan', action='store_true',
help='Perform quick scan (skip transitive dependencies)')
args = parser.parse_args()
try:
scanner = DependencyScanner()
results = scanner.scan_project(args.project_path)
report = scanner.generate_report(results, args.format)
if args.output:
with open(args.output, 'w') as f:
f.write(report)
print(f"Report saved to {args.output}")
else:
print(report)
# Exit with error if high-severity vulnerabilities found and --fail-on-high is set
if args.fail_on_high and results['high_severity_count'] > 0:
sys.exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
FILE:scripts/license_checker.py
#!/usr/bin/env python3
"""
License Checker - Dependency license compliance and conflict analysis tool.
This script analyzes dependency licenses from package metadata, classifies them
into risk categories, detects license conflicts, and generates compliance
reports with actionable recommendations for legal risk management.
Author: Claude Skills Engineering Team
License: MIT
"""
import json
import os
import sys
import argparse
from typing import Dict, List, Set, Any, Optional, Tuple
from pathlib import Path
from dataclasses import dataclass, asdict
from datetime import datetime
import re
from enum import Enum
class LicenseType(Enum):
"""License classification types."""
PERMISSIVE = "permissive"
COPYLEFT_STRONG = "copyleft_strong"
COPYLEFT_WEAK = "copyleft_weak"
PROPRIETARY = "proprietary"
DUAL = "dual"
UNKNOWN = "unknown"
class RiskLevel(Enum):
"""Risk assessment levels."""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
@dataclass
class LicenseInfo:
"""Represents license information for a dependency."""
name: str
spdx_id: Optional[str]
license_type: LicenseType
risk_level: RiskLevel
description: str
restrictions: List[str]
obligations: List[str]
compatibility: Dict[str, bool]
@dataclass
class DependencyLicense:
"""Represents a dependency with its license information."""
name: str
version: str
ecosystem: str
direct: bool
license_declared: Optional[str]
license_detected: Optional[LicenseInfo]
license_files: List[str]
confidence: float
@dataclass
class LicenseConflict:
"""Represents a license compatibility conflict."""
dependency1: str
license1: str
dependency2: str
license2: str
conflict_type: str
severity: RiskLevel
description: str
resolution_options: List[str]
class LicenseChecker:
"""Main license checking and compliance analysis class."""
def __init__(self):
self.license_database = self._build_license_database()
self.compatibility_matrix = self._build_compatibility_matrix()
self.license_patterns = self._build_license_patterns()
def _build_license_database(self) -> Dict[str, LicenseInfo]:
"""Build comprehensive license database with risk classifications."""
return {
# Permissive Licenses (Low Risk)
'MIT': LicenseInfo(
name='MIT License',
spdx_id='MIT',
license_type=LicenseType.PERMISSIVE,
risk_level=RiskLevel.LOW,
description='Very permissive license with minimal restrictions',
restrictions=['Include copyright notice', 'Include license text'],
obligations=['Attribution'],
compatibility={
'commercial': True, 'modification': True, 'distribution': True,
'private_use': True, 'patent_grant': False
}
),
'Apache-2.0': LicenseInfo(
name='Apache License 2.0',
spdx_id='Apache-2.0',
license_type=LicenseType.PERMISSIVE,
risk_level=RiskLevel.LOW,
description='Permissive license with patent protection',
restrictions=['Include copyright notice', 'Include license text',
'State changes', 'Include NOTICE file'],
obligations=['Attribution', 'Patent grant'],
compatibility={
'commercial': True, 'modification': True, 'distribution': True,
'private_use': True, 'patent_grant': True
}
),
'BSD-3-Clause': LicenseInfo(
name='BSD 3-Clause License',
spdx_id='BSD-3-Clause',
license_type=LicenseType.PERMISSIVE,
risk_level=RiskLevel.LOW,
description='Permissive license with non-endorsement clause',
restrictions=['Include copyright notice', 'Include license text',
'No endorsement using author names'],
obligations=['Attribution'],
compatibility={
'commercial': True, 'modification': True, 'distribution': True,
'private_use': True, 'patent_grant': False
}
),
'BSD-2-Clause': LicenseInfo(
name='BSD 2-Clause License',
spdx_id='BSD-2-Clause',
license_type=LicenseType.PERMISSIVE,
risk_level=RiskLevel.LOW,
description='Very permissive license similar to MIT',
restrictions=['Include copyright notice', 'Include license text'],
obligations=['Attribution'],
compatibility={
'commercial': True, 'modification': True, 'distribution': True,
'private_use': True, 'patent_grant': False
}
),
'ISC': LicenseInfo(
name='ISC License',
spdx_id='ISC',
license_type=LicenseType.PERMISSIVE,
risk_level=RiskLevel.LOW,
description='Functionally equivalent to MIT license',
restrictions=['Include copyright notice'],
obligations=['Attribution'],
compatibility={
'commercial': True, 'modification': True, 'distribution': True,
'private_use': True, 'patent_grant': False
}
),
# Weak Copyleft Licenses (Medium Risk)
'MPL-2.0': LicenseInfo(
name='Mozilla Public License 2.0',
spdx_id='MPL-2.0',
license_type=LicenseType.COPYLEFT_WEAK,
risk_level=RiskLevel.MEDIUM,
description='File-level copyleft license',
restrictions=['Disclose source of modified files', 'Include copyright notice',
'Include license text', 'State changes'],
obligations=['Source disclosure (modified files only)'],
compatibility={
'commercial': True, 'modification': True, 'distribution': True,
'private_use': True, 'patent_grant': True
}
),
'LGPL-2.1': LicenseInfo(
name='GNU Lesser General Public License 2.1',
spdx_id='LGPL-2.1',
license_type=LicenseType.COPYLEFT_WEAK,
risk_level=RiskLevel.MEDIUM,
description='Library-level copyleft license',
restrictions=['Disclose source of library modifications', 'Include copyright notice',
'Include license text', 'Allow relinking'],
obligations=['Source disclosure (library modifications)', 'Dynamic linking preferred'],
compatibility={
'commercial': True, 'modification': True, 'distribution': True,
'private_use': True, 'patent_grant': False
}
),
'LGPL-3.0': LicenseInfo(
name='GNU Lesser General Public License 3.0',
spdx_id='LGPL-3.0',
license_type=LicenseType.COPYLEFT_WEAK,
risk_level=RiskLevel.MEDIUM,
description='Library-level copyleft with patent provisions',
restrictions=['Disclose source of library modifications', 'Include copyright notice',
'Include license text', 'Allow relinking', 'Anti-tivoization'],
obligations=['Source disclosure (library modifications)', 'Patent grant'],
compatibility={
'commercial': True, 'modification': True, 'distribution': True,
'private_use': True, 'patent_grant': True
}
),
# Strong Copyleft Licenses (High Risk)
'GPL-2.0': LicenseInfo(
name='GNU General Public License 2.0',
spdx_id='GPL-2.0',
license_type=LicenseType.COPYLEFT_STRONG,
risk_level=RiskLevel.HIGH,
description='Strong copyleft requiring full source disclosure',
restrictions=['Disclose entire source code', 'Include copyright notice',
'Include license text', 'Use same license'],
obligations=['Full source disclosure', 'License compatibility'],
compatibility={
'commercial': False, 'modification': True, 'distribution': True,
'private_use': True, 'patent_grant': False
}
),
'GPL-3.0': LicenseInfo(
name='GNU General Public License 3.0',
spdx_id='GPL-3.0',
license_type=LicenseType.COPYLEFT_STRONG,
risk_level=RiskLevel.HIGH,
description='Strong copyleft with patent and hardware provisions',
restrictions=['Disclose entire source code', 'Include copyright notice',
'Include license text', 'Use same license', 'Anti-tivoization'],
obligations=['Full source disclosure', 'Patent grant', 'License compatibility'],
compatibility={
'commercial': False, 'modification': True, 'distribution': True,
'private_use': True, 'patent_grant': True
}
),
'AGPL-3.0': LicenseInfo(
name='GNU Affero General Public License 3.0',
spdx_id='AGPL-3.0',
license_type=LicenseType.COPYLEFT_STRONG,
risk_level=RiskLevel.CRITICAL,
description='Network copyleft extending GPL to SaaS',
restrictions=['Disclose entire source code', 'Include copyright notice',
'Include license text', 'Use same license', 'Network use triggers copyleft'],
obligations=['Full source disclosure', 'Network service source disclosure'],
compatibility={
'commercial': False, 'modification': True, 'distribution': True,
'private_use': True, 'patent_grant': True
}
),
# Proprietary/Commercial Licenses (High Risk)
'PROPRIETARY': LicenseInfo(
name='Proprietary License',
spdx_id=None,
license_type=LicenseType.PROPRIETARY,
risk_level=RiskLevel.HIGH,
description='Commercial or custom proprietary license',
restrictions=['Varies by license', 'Often no redistribution',
'May require commercial license'],
obligations=['License agreement compliance', 'Payment obligations'],
compatibility={
'commercial': False, 'modification': False, 'distribution': False,
'private_use': True, 'patent_grant': False
}
),
# Unknown/Unlicensed (Critical Risk)
'UNKNOWN': LicenseInfo(
name='Unknown License',
spdx_id=None,
license_type=LicenseType.UNKNOWN,
risk_level=RiskLevel.CRITICAL,
description='No license detected or ambiguous licensing',
restrictions=['Unknown', 'Assume no rights granted'],
obligations=['Investigate and clarify licensing'],
compatibility={
'commercial': False, 'modification': False, 'distribution': False,
'private_use': False, 'patent_grant': False
}
)
}
def _build_compatibility_matrix(self) -> Dict[str, Dict[str, bool]]:
"""Build license compatibility matrix."""
return {
'MIT': {
'MIT': True, 'Apache-2.0': True, 'BSD-3-Clause': True, 'BSD-2-Clause': True,
'ISC': True, 'MPL-2.0': True, 'LGPL-2.1': True, 'LGPL-3.0': True,
'GPL-2.0': False, 'GPL-3.0': False, 'AGPL-3.0': False, 'PROPRIETARY': False
},
'Apache-2.0': {
'MIT': True, 'Apache-2.0': True, 'BSD-3-Clause': True, 'BSD-2-Clause': True,
'ISC': True, 'MPL-2.0': True, 'LGPL-2.1': False, 'LGPL-3.0': True,
'GPL-2.0': False, 'GPL-3.0': True, 'AGPL-3.0': True, 'PROPRIETARY': False
},
'GPL-2.0': {
'MIT': True, 'Apache-2.0': False, 'BSD-3-Clause': True, 'BSD-2-Clause': True,
'ISC': True, 'MPL-2.0': False, 'LGPL-2.1': True, 'LGPL-3.0': False,
'GPL-2.0': True, 'GPL-3.0': False, 'AGPL-3.0': False, 'PROPRIETARY': False
},
'GPL-3.0': {
'MIT': True, 'Apache-2.0': True, 'BSD-3-Clause': True, 'BSD-2-Clause': True,
'ISC': True, 'MPL-2.0': True, 'LGPL-2.1': False, 'LGPL-3.0': True,
'GPL-2.0': False, 'GPL-3.0': True, 'AGPL-3.0': True, 'PROPRIETARY': False
},
'AGPL-3.0': {
'MIT': True, 'Apache-2.0': True, 'BSD-3-Clause': True, 'BSD-2-Clause': True,
'ISC': True, 'MPL-2.0': True, 'LGPL-2.1': False, 'LGPL-3.0': True,
'GPL-2.0': False, 'GPL-3.0': True, 'AGPL-3.0': True, 'PROPRIETARY': False
}
}
def _build_license_patterns(self) -> Dict[str, List[str]]:
"""Build license detection patterns for text analysis."""
return {
'MIT': [
r'MIT License',
r'Permission is hereby granted, free of charge',
r'THE SOFTWARE IS PROVIDED "AS IS"'
],
'Apache-2.0': [
r'Apache License, Version 2\.0',
r'Licensed under the Apache License',
r'http://www\.apache\.org/licenses/LICENSE-2\.0'
],
'GPL-2.0': [
r'GNU GENERAL PUBLIC LICENSE\s+Version 2',
r'This program is free software.*GPL.*version 2',
r'http://www\.gnu\.org/licenses/gpl-2\.0'
],
'GPL-3.0': [
r'GNU GENERAL PUBLIC LICENSE\s+Version 3',
r'This program is free software.*GPL.*version 3',
r'http://www\.gnu\.org/licenses/gpl-3\.0'
],
'BSD-3-Clause': [
r'BSD 3-Clause License',
r'Redistributions of source code must retain',
r'Neither the name.*may be used to endorse'
],
'BSD-2-Clause': [
r'BSD 2-Clause License',
r'Redistributions of source code must retain.*Redistributions in binary form'
]
}
def analyze_project(self, project_path: str, dependency_inventory: Optional[str] = None) -> Dict[str, Any]:
"""Analyze license compliance for a project."""
project_path = Path(project_path)
analysis_results = {
'timestamp': datetime.now().isoformat(),
'project_path': str(project_path),
'project_license': self._detect_project_license(project_path),
'dependencies': [],
'license_summary': {},
'conflicts': [],
'compliance_score': 0.0,
'risk_assessment': {},
'recommendations': []
}
# Load dependencies from inventory or scan project
if dependency_inventory:
dependencies = self._load_dependency_inventory(dependency_inventory)
else:
dependencies = self._scan_project_dependencies(project_path)
# Analyze each dependency's license
for dep in dependencies:
license_info = self._analyze_dependency_license(dep, project_path)
analysis_results['dependencies'].append(license_info)
# Generate license summary
analysis_results['license_summary'] = self._generate_license_summary(
analysis_results['dependencies']
)
# Detect conflicts
analysis_results['conflicts'] = self._detect_license_conflicts(
analysis_results['project_license'],
analysis_results['dependencies']
)
# Calculate compliance score
analysis_results['compliance_score'] = self._calculate_compliance_score(
analysis_results['dependencies'],
analysis_results['conflicts']
)
# Generate risk assessment
analysis_results['risk_assessment'] = self._generate_risk_assessment(
analysis_results['dependencies'],
analysis_results['conflicts']
)
# Generate recommendations
analysis_results['recommendations'] = self._generate_compliance_recommendations(
analysis_results
)
return analysis_results
def _detect_project_license(self, project_path: Path) -> Optional[str]:
"""Detect the main project license."""
license_files = ['LICENSE', 'LICENSE.txt', 'LICENSE.md', 'COPYING', 'COPYING.txt']
for license_file in license_files:
license_path = project_path / license_file
if license_path.exists():
try:
with open(license_path, 'r', encoding='utf-8') as f:
content = f.read()
# Analyze license content
detected_license = self._detect_license_from_text(content)
if detected_license:
return detected_license
except Exception as e:
print(f"Error reading license file {license_path}: {e}")
return None
def _detect_license_from_text(self, text: str) -> Optional[str]:
"""Detect license type from text content."""
text_upper = text.upper()
for license_id, patterns in self.license_patterns.items():
for pattern in patterns:
if re.search(pattern, text, re.IGNORECASE):
return license_id
# Common license text patterns
if 'MIT' in text_upper and 'PERMISSION IS HEREBY GRANTED' in text_upper:
return 'MIT'
elif 'APACHE LICENSE' in text_upper and 'VERSION 2.0' in text_upper:
return 'Apache-2.0'
elif 'GPL' in text_upper and 'VERSION 2' in text_upper:
return 'GPL-2.0'
elif 'GPL' in text_upper and 'VERSION 3' in text_upper:
return 'GPL-3.0'
return None
def _load_dependency_inventory(self, inventory_path: str) -> List[Dict[str, Any]]:
"""Load dependencies from JSON inventory file."""
try:
with open(inventory_path, 'r') as f:
data = json.load(f)
if 'dependencies' in data:
return data['dependencies']
else:
return data if isinstance(data, list) else []
except Exception as e:
print(f"Error loading dependency inventory: {e}")
return []
def _scan_project_dependencies(self, project_path: Path) -> List[Dict[str, Any]]:
"""Basic dependency scanning - in practice, would integrate with dep_scanner.py."""
dependencies = []
# Simple package.json parsing as example
package_json = project_path / 'package.json'
if package_json.exists():
try:
with open(package_json, 'r') as f:
data = json.load(f)
for dep_type in ['dependencies', 'devDependencies']:
if dep_type in data:
for name, version in data[dep_type].items():
dependencies.append({
'name': name,
'version': version,
'ecosystem': 'npm',
'direct': True
})
except Exception as e:
print(f"Error parsing package.json: {e}")
return dependencies
def _analyze_dependency_license(self, dependency: Dict[str, Any], project_path: Path) -> DependencyLicense:
"""Analyze license information for a single dependency."""
dep_license = DependencyLicense(
name=dependency['name'],
version=dependency.get('version', ''),
ecosystem=dependency.get('ecosystem', ''),
direct=dependency.get('direct', False),
license_declared=dependency.get('license'),
license_detected=None,
license_files=[],
confidence=0.0
)
# Try to detect license from various sources
declared_license = dependency.get('license')
if declared_license:
license_info = self._resolve_license_info(declared_license)
if license_info:
dep_license.license_detected = license_info
dep_license.confidence = 0.9
# For unknown licenses, try to find license files in node_modules (example)
if not dep_license.license_detected and dep_license.ecosystem == 'npm':
node_modules_path = project_path / 'node_modules' / dep_license.name
if node_modules_path.exists():
license_info = self._scan_package_directory(node_modules_path)
if license_info:
dep_license.license_detected = license_info
dep_license.confidence = 0.7
# Default to unknown if no license detected
if not dep_license.license_detected:
dep_license.license_detected = self.license_database['UNKNOWN']
dep_license.confidence = 0.0
return dep_license
def _resolve_license_info(self, license_string: str) -> Optional[LicenseInfo]:
"""Resolve license string to LicenseInfo object."""
if not license_string:
return None
license_string = license_string.strip()
# Direct SPDX ID match
if license_string in self.license_database:
return self.license_database[license_string]
# Common variations and mappings
license_mappings = {
'mit': 'MIT',
'apache': 'Apache-2.0',
'apache-2.0': 'Apache-2.0',
'apache 2.0': 'Apache-2.0',
'bsd': 'BSD-3-Clause',
'bsd-3-clause': 'BSD-3-Clause',
'bsd-2-clause': 'BSD-2-Clause',
'gpl-2.0': 'GPL-2.0',
'gpl-3.0': 'GPL-3.0',
'lgpl-2.1': 'LGPL-2.1',
'lgpl-3.0': 'LGPL-3.0',
'mpl-2.0': 'MPL-2.0',
'isc': 'ISC',
'unlicense': 'MIT', # Treat as permissive
'public domain': 'MIT', # Treat as permissive
'proprietary': 'PROPRIETARY',
'commercial': 'PROPRIETARY'
}
license_lower = license_string.lower()
for pattern, mapped_license in license_mappings.items():
if pattern in license_lower:
return self.license_database.get(mapped_license)
return None
def _scan_package_directory(self, package_path: Path) -> Optional[LicenseInfo]:
"""Scan package directory for license information."""
license_files = ['LICENSE', 'LICENSE.txt', 'LICENSE.md', 'COPYING', 'README.md', 'package.json']
for license_file in license_files:
file_path = package_path / license_file
if file_path.exists():
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Try to detect license from content
if license_file == 'package.json':
# Parse JSON for license field
try:
data = json.loads(content)
license_field = data.get('license')
if license_field:
return self._resolve_license_info(license_field)
except:
continue
else:
# Analyze text content
detected_license = self._detect_license_from_text(content)
if detected_license:
return self.license_database.get(detected_license)
except Exception:
continue
return None
def _generate_license_summary(self, dependencies: List[DependencyLicense]) -> Dict[str, Any]:
"""Generate summary of license distribution."""
summary = {
'total_dependencies': len(dependencies),
'license_types': {},
'risk_levels': {},
'unknown_licenses': 0,
'direct_dependencies': 0,
'transitive_dependencies': 0
}
for dep in dependencies:
# Count by license type
license_type = dep.license_detected.license_type.value
summary['license_types'][license_type] = summary['license_types'].get(license_type, 0) + 1
# Count by risk level
risk_level = dep.license_detected.risk_level.value
summary['risk_levels'][risk_level] = summary['risk_levels'].get(risk_level, 0) + 1
# Count unknowns
if dep.license_detected.license_type == LicenseType.UNKNOWN:
summary['unknown_licenses'] += 1
# Count direct vs transitive
if dep.direct:
summary['direct_dependencies'] += 1
else:
summary['transitive_dependencies'] += 1
return summary
def _detect_license_conflicts(self, project_license: Optional[str],
dependencies: List[DependencyLicense]) -> List[LicenseConflict]:
"""Detect license compatibility conflicts."""
conflicts = []
if not project_license:
# If no project license detected, flag as potential issue
for dep in dependencies:
if dep.license_detected.risk_level in [RiskLevel.HIGH, RiskLevel.CRITICAL]:
conflicts.append(LicenseConflict(
dependency1='Project',
license1='Unknown',
dependency2=dep.name,
license2=dep.license_detected.spdx_id or dep.license_detected.name,
conflict_type='Unknown project license',
severity=RiskLevel.HIGH,
description=f'Project license unknown, dependency {dep.name} has {dep.license_detected.risk_level.value} risk license',
resolution_options=['Define project license', 'Review dependency usage']
))
return conflicts
project_license_info = self.license_database.get(project_license)
if not project_license_info:
return conflicts
# Check compatibility with project license
for dep in dependencies:
dep_license_id = dep.license_detected.spdx_id or 'UNKNOWN'
# Check compatibility matrix
if project_license in self.compatibility_matrix:
compatibility = self.compatibility_matrix[project_license].get(dep_license_id, False)
if not compatibility:
severity = self._determine_conflict_severity(project_license_info, dep.license_detected)
conflicts.append(LicenseConflict(
dependency1='Project',
license1=project_license,
dependency2=dep.name,
license2=dep_license_id,
conflict_type='License incompatibility',
severity=severity,
description=f'Project license {project_license} is incompatible with dependency license {dep_license_id}',
resolution_options=self._generate_conflict_resolutions(project_license, dep_license_id)
))
# Check for GPL contamination in permissive projects
if project_license_info.license_type == LicenseType.PERMISSIVE:
for dep in dependencies:
if dep.license_detected.license_type == LicenseType.COPYLEFT_STRONG:
conflicts.append(LicenseConflict(
dependency1='Project',
license1=project_license,
dependency2=dep.name,
license2=dep.license_detected.spdx_id or dep.license_detected.name,
conflict_type='GPL contamination',
severity=RiskLevel.CRITICAL,
description=f'GPL dependency {dep.name} may contaminate permissive project',
resolution_options=['Remove GPL dependency', 'Change project license to GPL',
'Use dynamic linking', 'Find alternative dependency']
))
return conflicts
def _determine_conflict_severity(self, project_license: LicenseInfo, dep_license: LicenseInfo) -> RiskLevel:
"""Determine severity of a license conflict."""
if dep_license.license_type == LicenseType.UNKNOWN:
return RiskLevel.CRITICAL
elif (project_license.license_type == LicenseType.PERMISSIVE and
dep_license.license_type == LicenseType.COPYLEFT_STRONG):
return RiskLevel.CRITICAL
elif dep_license.license_type == LicenseType.PROPRIETARY:
return RiskLevel.HIGH
else:
return RiskLevel.MEDIUM
def _generate_conflict_resolutions(self, project_license: str, dep_license: str) -> List[str]:
"""Generate resolution options for license conflicts."""
resolutions = []
if 'GPL' in dep_license:
resolutions.extend([
'Find alternative non-GPL dependency',
'Use dynamic linking if possible',
'Consider changing project license to GPL-compatible',
'Remove the dependency if not essential'
])
elif dep_license == 'PROPRIETARY':
resolutions.extend([
'Obtain commercial license',
'Find open-source alternative',
'Remove dependency if not essential',
'Negotiate license terms'
])
else:
resolutions.extend([
'Review license compatibility carefully',
'Consult legal counsel',
'Find alternative dependency',
'Consider license exception'
])
return resolutions
def _calculate_compliance_score(self, dependencies: List[DependencyLicense],
conflicts: List[LicenseConflict]) -> float:
"""Calculate overall compliance score (0-100)."""
if not dependencies:
return 100.0
base_score = 100.0
# Deduct points for unknown licenses
unknown_count = sum(1 for dep in dependencies
if dep.license_detected.license_type == LicenseType.UNKNOWN)
base_score -= (unknown_count / len(dependencies)) * 30
# Deduct points for high-risk licenses
high_risk_count = sum(1 for dep in dependencies
if dep.license_detected.risk_level in [RiskLevel.HIGH, RiskLevel.CRITICAL])
base_score -= (high_risk_count / len(dependencies)) * 20
# Deduct points for conflicts
if conflicts:
critical_conflicts = sum(1 for c in conflicts if c.severity == RiskLevel.CRITICAL)
high_conflicts = sum(1 for c in conflicts if c.severity == RiskLevel.HIGH)
base_score -= critical_conflicts * 15
base_score -= high_conflicts * 10
return max(0.0, base_score)
def _generate_risk_assessment(self, dependencies: List[DependencyLicense],
conflicts: List[LicenseConflict]) -> Dict[str, Any]:
"""Generate comprehensive risk assessment."""
return {
'overall_risk': self._calculate_overall_risk(dependencies, conflicts),
'license_risk_breakdown': self._calculate_license_risks(dependencies),
'conflict_summary': {
'total_conflicts': len(conflicts),
'critical_conflicts': len([c for c in conflicts if c.severity == RiskLevel.CRITICAL]),
'high_conflicts': len([c for c in conflicts if c.severity == RiskLevel.HIGH])
},
'distribution_risks': self._assess_distribution_risks(dependencies),
'commercial_risks': self._assess_commercial_risks(dependencies)
}
def _calculate_overall_risk(self, dependencies: List[DependencyLicense],
conflicts: List[LicenseConflict]) -> str:
"""Calculate overall project risk level."""
if any(c.severity == RiskLevel.CRITICAL for c in conflicts):
return 'CRITICAL'
elif any(dep.license_detected.risk_level == RiskLevel.CRITICAL for dep in dependencies):
return 'CRITICAL'
elif any(c.severity == RiskLevel.HIGH for c in conflicts):
return 'HIGH'
elif any(dep.license_detected.risk_level == RiskLevel.HIGH for dep in dependencies):
return 'HIGH'
elif any(dep.license_detected.risk_level == RiskLevel.MEDIUM for dep in dependencies):
return 'MEDIUM'
else:
return 'LOW'
def _calculate_license_risks(self, dependencies: List[DependencyLicense]) -> Dict[str, int]:
"""Calculate breakdown of license risks."""
risks = {'low': 0, 'medium': 0, 'high': 0, 'critical': 0}
for dep in dependencies:
risk_level = dep.license_detected.risk_level.value
risks[risk_level] += 1
return risks
def _assess_distribution_risks(self, dependencies: List[DependencyLicense]) -> List[str]:
"""Assess risks related to software distribution."""
risks = []
gpl_deps = [dep for dep in dependencies
if dep.license_detected.license_type == LicenseType.COPYLEFT_STRONG]
if gpl_deps:
risks.append(f"GPL dependencies require source code disclosure: {[d.name for d in gpl_deps]}")
proprietary_deps = [dep for dep in dependencies
if dep.license_detected.license_type == LicenseType.PROPRIETARY]
if proprietary_deps:
risks.append(f"Proprietary dependencies may require commercial licenses: {[d.name for d in proprietary_deps]}")
unknown_deps = [dep for dep in dependencies
if dep.license_detected.license_type == LicenseType.UNKNOWN]
if unknown_deps:
risks.append(f"Unknown licenses pose legal uncertainty: {[d.name for d in unknown_deps]}")
return risks
def _assess_commercial_risks(self, dependencies: List[DependencyLicense]) -> List[str]:
"""Assess risks for commercial usage."""
risks = []
agpl_deps = [dep for dep in dependencies
if dep.license_detected.spdx_id == 'AGPL-3.0']
if agpl_deps:
risks.append(f"AGPL dependencies trigger copyleft for network services: {[d.name for d in agpl_deps]}")
return risks
def _generate_compliance_recommendations(self, analysis_results: Dict[str, Any]) -> List[str]:
"""Generate actionable compliance recommendations."""
recommendations = []
# Address critical issues first
critical_conflicts = [c for c in analysis_results['conflicts']
if c.severity == RiskLevel.CRITICAL]
if critical_conflicts:
recommendations.append("CRITICAL: Address license conflicts immediately before any distribution")
for conflict in critical_conflicts[:3]: # Top 3
recommendations.append(f" • {conflict.description}")
# Unknown licenses
unknown_count = analysis_results['license_summary']['unknown_licenses']
if unknown_count > 0:
recommendations.append(f"Investigate and clarify licenses for {unknown_count} dependencies with unknown licensing")
# GPL contamination
gpl_deps = [dep for dep in analysis_results['dependencies']
if dep.license_detected.license_type == LicenseType.COPYLEFT_STRONG]
if gpl_deps and analysis_results.get('project_license') in ['MIT', 'Apache-2.0', 'BSD-3-Clause']:
recommendations.append("Consider removing GPL dependencies or changing project license for permissive project")
# Compliance score
if analysis_results['compliance_score'] < 70:
recommendations.append("Overall compliance score is low - prioritize license cleanup")
return recommendations
def generate_report(self, analysis_results: Dict[str, Any], format: str = 'text') -> str:
"""Generate compliance report in specified format."""
if format == 'json':
# Convert dataclass objects for JSON serialization
serializable_results = analysis_results.copy()
serializable_results['dependencies'] = [
{
'name': dep.name,
'version': dep.version,
'ecosystem': dep.ecosystem,
'direct': dep.direct,
'license_declared': dep.license_declared,
'license_detected': asdict(dep.license_detected) if dep.license_detected else None,
'confidence': dep.confidence
}
for dep in analysis_results['dependencies']
]
serializable_results['conflicts'] = [asdict(conflict) for conflict in analysis_results['conflicts']]
return json.dumps(serializable_results, indent=2, default=str)
# Text format report
report = []
report.append("=" * 60)
report.append("LICENSE COMPLIANCE REPORT")
report.append("=" * 60)
report.append(f"Analysis Date: {analysis_results['timestamp']}")
report.append(f"Project: {analysis_results['project_path']}")
report.append(f"Project License: {analysis_results['project_license'] or 'Unknown'}")
report.append("")
# Summary
summary = analysis_results['license_summary']
report.append("SUMMARY:")
report.append(f" Total Dependencies: {summary['total_dependencies']}")
report.append(f" Compliance Score: {analysis_results['compliance_score']:.1f}/100")
report.append(f" Overall Risk: {analysis_results['risk_assessment']['overall_risk']}")
report.append(f" License Conflicts: {len(analysis_results['conflicts'])}")
report.append("")
# License distribution
report.append("LICENSE DISTRIBUTION:")
for license_type, count in summary['license_types'].items():
report.append(f" {license_type.title()}: {count}")
report.append("")
# Risk breakdown
report.append("RISK BREAKDOWN:")
for risk_level, count in summary['risk_levels'].items():
report.append(f" {risk_level.title()}: {count}")
report.append("")
# Conflicts
if analysis_results['conflicts']:
report.append("LICENSE CONFLICTS:")
report.append("-" * 30)
for conflict in analysis_results['conflicts']:
report.append(f"Conflict: {conflict.dependency2} ({conflict.license2})")
report.append(f" Issue: {conflict.description}")
report.append(f" Severity: {conflict.severity.value.upper()}")
report.append(f" Resolutions: {', '.join(conflict.resolution_options[:2])}")
report.append("")
# High-risk dependencies
high_risk_deps = [dep for dep in analysis_results['dependencies']
if dep.license_detected.risk_level in [RiskLevel.HIGH, RiskLevel.CRITICAL]]
if high_risk_deps:
report.append("HIGH-RISK DEPENDENCIES:")
report.append("-" * 30)
for dep in high_risk_deps[:10]: # Top 10
license_name = dep.license_detected.spdx_id or dep.license_detected.name
report.append(f" {dep.name} v{dep.version}: {license_name} ({dep.license_detected.risk_level.value.upper()})")
report.append("")
# Recommendations
if analysis_results['recommendations']:
report.append("RECOMMENDATIONS:")
report.append("-" * 20)
for i, rec in enumerate(analysis_results['recommendations'], 1):
report.append(f"{i}. {rec}")
report.append("")
report.append("=" * 60)
return '\n'.join(report)
def main():
"""Main entry point for the license checker."""
parser = argparse.ArgumentParser(
description='Analyze dependency licenses for compliance and conflicts',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python license_checker.py /path/to/project
python license_checker.py . --format json --output compliance.json
python license_checker.py /app --inventory deps.json --policy strict
"""
)
parser.add_argument('project_path',
help='Path to the project directory to analyze')
parser.add_argument('--inventory',
help='Path to dependency inventory JSON file')
parser.add_argument('--format', choices=['text', 'json'], default='text',
help='Output format (default: text)')
parser.add_argument('--output', '-o',
help='Output file path (default: stdout)')
parser.add_argument('--policy', choices=['permissive', 'strict'], default='permissive',
help='License policy strictness (default: permissive)')
parser.add_argument('--warn-conflicts', action='store_true',
help='Show warnings for potential conflicts')
args = parser.parse_args()
try:
checker = LicenseChecker()
results = checker.analyze_project(args.project_path, args.inventory)
report = checker.generate_report(results, args.format)
if args.output:
with open(args.output, 'w') as f:
f.write(report)
print(f"Compliance report saved to {args.output}")
else:
print(report)
# Exit with error code for policy violations
if args.policy == 'strict' and results['compliance_score'] < 80:
sys.exit(1)
if args.warn_conflicts and results['conflicts']:
print("\nWARNING: License conflicts detected!")
sys.exit(2)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
FILE:scripts/upgrade_planner.py
#!/usr/bin/env python3
"""
Upgrade Planner - Dependency upgrade path planning and risk analysis tool.
This script analyzes dependency inventories, evaluates semantic versioning patterns,
estimates breaking change risks, and generates prioritized upgrade plans with
migration checklists and rollback procedures.
Author: Claude Skills Engineering Team
License: MIT
"""
import json
import os
import sys
import argparse
from typing import Dict, List, Set, Any, Optional, Tuple
from pathlib import Path
from dataclasses import dataclass, asdict
from datetime import datetime, timedelta
from enum import Enum
import re
import subprocess
class UpgradeRisk(Enum):
"""Upgrade risk levels."""
SAFE = "safe"
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class UpdateType(Enum):
"""Semantic versioning update types."""
PATCH = "patch"
MINOR = "minor"
MAJOR = "major"
PRERELEASE = "prerelease"
@dataclass
class VersionInfo:
"""Represents version information."""
major: int
minor: int
patch: int
prerelease: Optional[str] = None
build: Optional[str] = None
def __str__(self):
version = f"{self.major}.{self.minor}.{self.patch}"
if self.prerelease:
version += f"-{self.prerelease}"
if self.build:
version += f"+{self.build}"
return version
@dataclass
class DependencyUpgrade:
"""Represents a potential dependency upgrade."""
name: str
current_version: str
latest_version: str
ecosystem: str
direct: bool
update_type: UpdateType
risk_level: UpgradeRisk
security_updates: List[str]
breaking_changes: List[str]
migration_effort: str
dependencies_affected: List[str]
rollback_complexity: str
estimated_time: str
priority_score: float
@dataclass
class UpgradePlan:
"""Represents a complete upgrade plan."""
name: str
description: str
phase: int
dependencies: List[str]
estimated_duration: str
prerequisites: List[str]
migration_steps: List[str]
testing_requirements: List[str]
rollback_plan: List[str]
success_criteria: List[str]
class UpgradePlanner:
"""Main upgrade planning and risk analysis class."""
def __init__(self):
self.breaking_change_patterns = self._build_breaking_change_patterns()
self.ecosystem_knowledge = self._build_ecosystem_knowledge()
self.security_advisories = self._build_security_advisories()
def _build_breaking_change_patterns(self) -> Dict[str, List[str]]:
"""Build patterns for detecting breaking changes."""
return {
'npm': [
r'BREAKING\s*CHANGE',
r'breaking\s*change',
r'major\s*version',
r'removed.*API',
r'deprecated.*removed',
r'no\s*longer\s*supported',
r'minimum.*node.*version',
r'peer.*dependency.*change'
],
'pypi': [
r'BREAKING\s*CHANGE',
r'breaking\s*change',
r'removed.*function',
r'deprecated.*removed',
r'minimum.*python.*version',
r'incompatible.*change',
r'API.*change'
],
'maven': [
r'BREAKING\s*CHANGE',
r'breaking\s*change',
r'removed.*method',
r'deprecated.*removed',
r'minimum.*java.*version',
r'API.*incompatible'
]
}
def _build_ecosystem_knowledge(self) -> Dict[str, Dict[str, Any]]:
"""Build ecosystem-specific upgrade knowledge."""
return {
'npm': {
'typical_major_cycle_months': 12,
'typical_patch_cycle_weeks': 2,
'deprecation_notice_months': 6,
'lts_support_years': 3,
'common_breaking_changes': [
'Node.js version requirements',
'Peer dependency updates',
'API signature changes',
'Configuration format changes'
]
},
'pypi': {
'typical_major_cycle_months': 18,
'typical_patch_cycle_weeks': 4,
'deprecation_notice_months': 12,
'lts_support_years': 2,
'common_breaking_changes': [
'Python version requirements',
'Function signature changes',
'Import path changes',
'Configuration changes'
]
},
'maven': {
'typical_major_cycle_months': 24,
'typical_patch_cycle_weeks': 6,
'deprecation_notice_months': 12,
'lts_support_years': 5,
'common_breaking_changes': [
'Java version requirements',
'Method signature changes',
'Package restructuring',
'Dependency changes'
]
},
'cargo': {
'typical_major_cycle_months': 6,
'typical_patch_cycle_weeks': 2,
'deprecation_notice_months': 3,
'lts_support_years': 1,
'common_breaking_changes': [
'Rust edition changes',
'Trait changes',
'Module restructuring',
'Macro changes'
]
}
}
def _build_security_advisories(self) -> Dict[str, List[Dict[str, Any]]]:
"""Build security advisory database for upgrade prioritization."""
return {
'lodash': [
{
'advisory_id': 'CVE-2021-23337',
'severity': 'HIGH',
'fixed_in': '4.17.21',
'description': 'Prototype pollution vulnerability'
}
],
'django': [
{
'advisory_id': 'CVE-2024-27351',
'severity': 'HIGH',
'fixed_in': '4.2.11',
'description': 'SQL injection vulnerability'
}
],
'express': [
{
'advisory_id': 'CVE-2022-24999',
'severity': 'MEDIUM',
'fixed_in': '4.18.2',
'description': 'Open redirect vulnerability'
}
],
'axios': [
{
'advisory_id': 'CVE-2023-45857',
'severity': 'MEDIUM',
'fixed_in': '1.6.0',
'description': 'Cross-site request forgery'
}
]
}
def analyze_upgrades(self, dependency_inventory: str, timeline_days: int = 90) -> Dict[str, Any]:
"""Analyze potential dependency upgrades and create upgrade plan."""
dependencies = self._load_dependency_inventory(dependency_inventory)
analysis_results = {
'timestamp': datetime.now().isoformat(),
'timeline_days': timeline_days,
'dependencies_analyzed': len(dependencies),
'available_upgrades': [],
'upgrade_statistics': {},
'risk_assessment': {},
'upgrade_plans': [],
'recommendations': []
}
# Analyze each dependency for upgrades
for dep in dependencies:
upgrade_info = self._analyze_dependency_upgrade(dep)
if upgrade_info:
analysis_results['available_upgrades'].append(upgrade_info)
# Generate upgrade statistics
analysis_results['upgrade_statistics'] = self._generate_upgrade_statistics(
analysis_results['available_upgrades']
)
# Perform risk assessment
analysis_results['risk_assessment'] = self._perform_risk_assessment(
analysis_results['available_upgrades']
)
# Create phased upgrade plans
analysis_results['upgrade_plans'] = self._create_upgrade_plans(
analysis_results['available_upgrades'],
timeline_days
)
# Generate recommendations
analysis_results['recommendations'] = self._generate_upgrade_recommendations(
analysis_results
)
return analysis_results
def _load_dependency_inventory(self, inventory_path: str) -> List[Dict[str, Any]]:
"""Load dependency inventory from JSON file."""
try:
with open(inventory_path, 'r') as f:
data = json.load(f)
if 'dependencies' in data:
return data['dependencies']
elif isinstance(data, list):
return data
else:
print("Warning: Unexpected inventory format")
return []
except Exception as e:
print(f"Error loading dependency inventory: {e}")
return []
def _analyze_dependency_upgrade(self, dependency: Dict[str, Any]) -> Optional[DependencyUpgrade]:
"""Analyze upgrade possibilities for a single dependency."""
name = dependency.get('name', '')
current_version = dependency.get('version', '').replace('^', '').replace('~', '')
ecosystem = dependency.get('ecosystem', '')
if not name or not current_version:
return None
# Parse current version
current_ver = self._parse_version(current_version)
if not current_ver:
return None
# Get latest version (simulated - in practice would query package registries)
latest_version = self._get_latest_version(name, ecosystem)
if not latest_version:
return None
latest_ver = self._parse_version(latest_version)
if not latest_ver:
return None
# Determine if upgrade is needed
if self._compare_versions(current_ver, latest_ver) >= 0:
return None # Already up to date
# Determine update type
update_type = self._determine_update_type(current_ver, latest_ver)
# Assess upgrade risk
risk_level = self._assess_upgrade_risk(name, current_ver, latest_ver, ecosystem, update_type)
# Check for security updates
security_updates = self._check_security_updates(name, current_version, latest_version)
# Analyze breaking changes
breaking_changes = self._analyze_breaking_changes(name, current_ver, latest_ver, ecosystem)
# Calculate priority score
priority_score = self._calculate_priority_score(
update_type, risk_level, security_updates, dependency.get('direct', False)
)
return DependencyUpgrade(
name=name,
current_version=current_version,
latest_version=latest_version,
ecosystem=ecosystem,
direct=dependency.get('direct', False),
update_type=update_type,
risk_level=risk_level,
security_updates=security_updates,
breaking_changes=breaking_changes,
migration_effort=self._estimate_migration_effort(update_type, breaking_changes),
dependencies_affected=self._get_affected_dependencies(name, dependency),
rollback_complexity=self._assess_rollback_complexity(update_type, risk_level),
estimated_time=self._estimate_upgrade_time(update_type, breaking_changes),
priority_score=priority_score
)
def _parse_version(self, version_string: str) -> Optional[VersionInfo]:
"""Parse semantic version string."""
# Clean version string
version = re.sub(r'[^0-9a-zA-Z.-]', '', version_string)
# Basic semver pattern
pattern = r'^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$'
match = re.match(pattern, version)
if match:
major, minor, patch, prerelease, build = match.groups()
return VersionInfo(
major=int(major),
minor=int(minor),
patch=int(patch),
prerelease=prerelease,
build=build
)
# Fallback for simpler version patterns
simple_pattern = r'^(\d+)\.(\d+)(?:\.(\d+))?'
match = re.match(simple_pattern, version)
if match:
major, minor, patch = match.groups()
return VersionInfo(
major=int(major),
minor=int(minor),
patch=int(patch or 0)
)
return None
def _compare_versions(self, v1: VersionInfo, v2: VersionInfo) -> int:
"""Compare two versions. Returns -1, 0, or 1."""
if (v1.major, v1.minor, v1.patch) < (v2.major, v2.minor, v2.patch):
return -1
elif (v1.major, v1.minor, v1.patch) > (v2.major, v2.minor, v2.patch):
return 1
else:
# Handle prerelease comparison
if v1.prerelease and not v2.prerelease:
return -1
elif not v1.prerelease and v2.prerelease:
return 1
elif v1.prerelease and v2.prerelease:
if v1.prerelease < v2.prerelease:
return -1
elif v1.prerelease > v2.prerelease:
return 1
return 0
def _get_latest_version(self, package_name: str, ecosystem: str) -> Optional[str]:
"""Get latest version from package registry (simulated)."""
# Simulated latest versions for common packages
mock_versions = {
'lodash': '4.17.21',
'express': '4.18.2',
'react': '18.2.0',
'axios': '1.6.0',
'django': '4.2.11',
'requests': '2.31.0',
'numpy': '1.24.0',
'flask': '2.3.0',
'fastapi': '0.104.0',
'pytest': '7.4.0'
}
# In production, would query actual package registries:
# npm: npm view <package> version
# pypi: pip index versions <package>
# maven: maven metadata API
return mock_versions.get(package_name.lower())
def _determine_update_type(self, current: VersionInfo, latest: VersionInfo) -> UpdateType:
"""Determine the type of update based on semantic versioning."""
if latest.major > current.major:
return UpdateType.MAJOR
elif latest.minor > current.minor:
return UpdateType.MINOR
elif latest.patch > current.patch:
return UpdateType.PATCH
elif latest.prerelease and not current.prerelease:
return UpdateType.PRERELEASE
else:
return UpdateType.PATCH # Default fallback
def _assess_upgrade_risk(self, package_name: str, current: VersionInfo, latest: VersionInfo,
ecosystem: str, update_type: UpdateType) -> UpgradeRisk:
"""Assess the risk level of an upgrade."""
# Base risk assessment on update type
base_risk = {
UpdateType.PATCH: UpgradeRisk.SAFE,
UpdateType.MINOR: UpgradeRisk.LOW,
UpdateType.MAJOR: UpgradeRisk.HIGH,
UpdateType.PRERELEASE: UpgradeRisk.MEDIUM
}.get(update_type, UpgradeRisk.MEDIUM)
# Adjust for package-specific factors
high_risk_packages = [
'webpack', 'babel', 'typescript', 'eslint', # Build tools
'react', 'vue', 'angular', # Frameworks
'django', 'flask', 'fastapi', # Web frameworks
'spring-boot', 'hibernate' # Java frameworks
]
if package_name.lower() in high_risk_packages and update_type == UpdateType.MAJOR:
base_risk = UpgradeRisk.CRITICAL
# Check for known breaking changes
if self._has_known_breaking_changes(package_name, current, latest):
if base_risk in [UpgradeRisk.SAFE, UpgradeRisk.LOW]:
base_risk = UpgradeRisk.MEDIUM
elif base_risk == UpgradeRisk.MEDIUM:
base_risk = UpgradeRisk.HIGH
return base_risk
def _has_known_breaking_changes(self, package_name: str, current: VersionInfo, latest: VersionInfo) -> bool:
"""Check if there are known breaking changes between versions."""
# Simulated breaking change detection
breaking_change_versions = {
'react': ['16.0.0', '17.0.0', '18.0.0'],
'django': ['2.0.0', '3.0.0', '4.0.0'],
'webpack': ['4.0.0', '5.0.0'],
'babel': ['7.0.0', '8.0.0'],
'typescript': ['4.0.0', '5.0.0']
}
package_versions = breaking_change_versions.get(package_name.lower(), [])
latest_str = str(latest)
return any(latest_str.startswith(v.split('.')[0]) for v in package_versions)
def _check_security_updates(self, package_name: str, current_version: str, latest_version: str) -> List[str]:
"""Check for security updates in the upgrade."""
security_updates = []
if package_name in self.security_advisories:
for advisory in self.security_advisories[package_name]:
fixed_version = advisory['fixed_in']
# Simple version comparison for security fixes
if (self._is_version_greater(fixed_version, current_version) and
not self._is_version_greater(fixed_version, latest_version)):
security_updates.append(f"{advisory['advisory_id']}: {advisory['description']}")
return security_updates
def _is_version_greater(self, v1: str, v2: str) -> bool:
"""Simple version comparison."""
v1_parts = [int(x) for x in v1.split('.')]
v2_parts = [int(x) for x in v2.split('.')]
# Pad shorter version
max_len = max(len(v1_parts), len(v2_parts))
v1_parts.extend([0] * (max_len - len(v1_parts)))
v2_parts.extend([0] * (max_len - len(v2_parts)))
return v1_parts > v2_parts
def _analyze_breaking_changes(self, package_name: str, current: VersionInfo,
latest: VersionInfo, ecosystem: str) -> List[str]:
"""Analyze potential breaking changes."""
breaking_changes = []
# Check if major version change
if latest.major > current.major:
breaking_changes.append(f"Major version upgrade from {current.major}.x to {latest.major}.x")
# Add ecosystem-specific common breaking changes
ecosystem_knowledge = self.ecosystem_knowledge.get(ecosystem, {})
common_changes = ecosystem_knowledge.get('common_breaking_changes', [])
breaking_changes.extend(common_changes[:2]) # Add top 2
# Check for specific package patterns
if package_name.lower() == 'react' and latest.major >= 17:
breaking_changes.append("New JSX Transform")
if latest.major >= 18:
breaking_changes.append("Concurrent Rendering changes")
elif package_name.lower() == 'django' and latest.major >= 4:
breaking_changes.append("CSRF token changes")
breaking_changes.append("Default AUTO_INCREMENT field changes")
elif package_name.lower() == 'webpack' and latest.major >= 5:
breaking_changes.append("Module Federation support")
breaking_changes.append("Asset modules replace file-loader")
return breaking_changes
def _calculate_priority_score(self, update_type: UpdateType, risk_level: UpgradeRisk,
security_updates: List[str], is_direct: bool) -> float:
"""Calculate priority score for upgrade (0-100)."""
score = 50.0 # Base score
# Security updates get highest priority
if security_updates:
score += 30.0
score += len(security_updates) * 5.0 # Multiple security fixes
# Update type scoring
type_scores = {
UpdateType.PATCH: 20.0,
UpdateType.MINOR: 10.0,
UpdateType.MAJOR: -10.0,
UpdateType.PRERELEASE: -5.0
}
score += type_scores.get(update_type, 0)
# Risk level adjustment
risk_adjustments = {
UpgradeRisk.SAFE: 15.0,
UpgradeRisk.LOW: 5.0,
UpgradeRisk.MEDIUM: -5.0,
UpgradeRisk.HIGH: -15.0,
UpgradeRisk.CRITICAL: -25.0
}
score += risk_adjustments.get(risk_level, 0)
# Direct dependencies get slightly higher priority
if is_direct:
score += 5.0
return max(0.0, min(100.0, score))
def _estimate_migration_effort(self, update_type: UpdateType, breaking_changes: List[str]) -> str:
"""Estimate migration effort level."""
if update_type == UpdateType.PATCH and not breaking_changes:
return "Minimal"
elif update_type == UpdateType.MINOR and len(breaking_changes) <= 1:
return "Low"
elif update_type == UpdateType.MAJOR or len(breaking_changes) > 2:
return "High"
else:
return "Medium"
def _get_affected_dependencies(self, package_name: str, dependency: Dict[str, Any]) -> List[str]:
"""Get list of dependencies that might be affected by this upgrade."""
# Simulated dependency impact analysis
common_dependencies = {
'react': ['react-dom', 'react-router', 'react-redux'],
'django': ['djangorestframework', 'django-cors-headers', 'celery'],
'webpack': ['webpack-cli', 'webpack-dev-server', 'html-webpack-plugin'],
'babel': ['@babel/core', '@babel/preset-env', '@babel/preset-react']
}
return common_dependencies.get(package_name.lower(), [])
def _assess_rollback_complexity(self, update_type: UpdateType, risk_level: UpgradeRisk) -> str:
"""Assess complexity of rolling back the upgrade."""
if update_type == UpdateType.PATCH:
return "Simple"
elif update_type == UpdateType.MINOR and risk_level in [UpgradeRisk.SAFE, UpgradeRisk.LOW]:
return "Simple"
elif risk_level in [UpgradeRisk.HIGH, UpgradeRisk.CRITICAL]:
return "Complex"
else:
return "Moderate"
def _estimate_upgrade_time(self, update_type: UpdateType, breaking_changes: List[str]) -> str:
"""Estimate time required for upgrade."""
base_times = {
UpdateType.PATCH: "30 minutes",
UpdateType.MINOR: "2 hours",
UpdateType.MAJOR: "1 day",
UpdateType.PRERELEASE: "4 hours"
}
base_time = base_times.get(update_type, "4 hours")
if len(breaking_changes) > 2:
if "30 minutes" in base_time:
base_time = "2 hours"
elif "2 hours" in base_time:
base_time = "1 day"
elif "1 day" in base_time:
base_time = "3 days"
return base_time
def _generate_upgrade_statistics(self, upgrades: List[DependencyUpgrade]) -> Dict[str, Any]:
"""Generate statistics about available upgrades."""
if not upgrades:
return {}
return {
'total_upgrades': len(upgrades),
'by_type': {
'patch': len([u for u in upgrades if u.update_type == UpdateType.PATCH]),
'minor': len([u for u in upgrades if u.update_type == UpdateType.MINOR]),
'major': len([u for u in upgrades if u.update_type == UpdateType.MAJOR]),
'prerelease': len([u for u in upgrades if u.update_type == UpdateType.PRERELEASE])
},
'by_risk': {
'safe': len([u for u in upgrades if u.risk_level == UpgradeRisk.SAFE]),
'low': len([u for u in upgrades if u.risk_level == UpgradeRisk.LOW]),
'medium': len([u for u in upgrades if u.risk_level == UpgradeRisk.MEDIUM]),
'high': len([u for u in upgrades if u.risk_level == UpgradeRisk.HIGH]),
'critical': len([u for u in upgrades if u.risk_level == UpgradeRisk.CRITICAL])
},
'security_updates': len([u for u in upgrades if u.security_updates]),
'direct_dependencies': len([u for u in upgrades if u.direct]),
'average_priority': sum(u.priority_score for u in upgrades) / len(upgrades)
}
def _perform_risk_assessment(self, upgrades: List[DependencyUpgrade]) -> Dict[str, Any]:
"""Perform comprehensive risk assessment."""
high_risk_upgrades = [u for u in upgrades if u.risk_level in [UpgradeRisk.HIGH, UpgradeRisk.CRITICAL]]
security_upgrades = [u for u in upgrades if u.security_updates]
major_upgrades = [u for u in upgrades if u.update_type == UpdateType.MAJOR]
return {
'overall_risk': self._calculate_overall_upgrade_risk(upgrades),
'high_risk_count': len(high_risk_upgrades),
'security_critical_count': len(security_upgrades),
'major_version_count': len(major_upgrades),
'risk_factors': self._identify_risk_factors(upgrades),
'mitigation_strategies': self._suggest_mitigation_strategies(upgrades)
}
def _calculate_overall_upgrade_risk(self, upgrades: List[DependencyUpgrade]) -> str:
"""Calculate overall risk level for all upgrades."""
if not upgrades:
return "LOW"
risk_scores = {
UpgradeRisk.SAFE: 1,
UpgradeRisk.LOW: 2,
UpgradeRisk.MEDIUM: 3,
UpgradeRisk.HIGH: 4,
UpgradeRisk.CRITICAL: 5
}
total_score = sum(risk_scores.get(u.risk_level, 3) for u in upgrades)
average_score = total_score / len(upgrades)
if average_score >= 4.0:
return "CRITICAL"
elif average_score >= 3.0:
return "HIGH"
elif average_score >= 2.0:
return "MEDIUM"
else:
return "LOW"
def _identify_risk_factors(self, upgrades: List[DependencyUpgrade]) -> List[str]:
"""Identify key risk factors across all upgrades."""
factors = []
major_count = len([u for u in upgrades if u.update_type == UpdateType.MAJOR])
if major_count > 0:
factors.append(f"{major_count} major version upgrades with potential breaking changes")
critical_count = len([u for u in upgrades if u.risk_level == UpgradeRisk.CRITICAL])
if critical_count > 0:
factors.append(f"{critical_count} critical risk upgrades requiring careful planning")
framework_upgrades = [u for u in upgrades if any(fw in u.name.lower()
for fw in ['react', 'django', 'spring', 'webpack', 'babel'])]
if framework_upgrades:
factors.append(f"Core framework upgrades: {[u.name for u in framework_upgrades[:3]]}")
return factors
def _suggest_mitigation_strategies(self, upgrades: List[DependencyUpgrade]) -> List[str]:
"""Suggest risk mitigation strategies."""
strategies = []
high_risk_count = len([u for u in upgrades if u.risk_level in [UpgradeRisk.HIGH, UpgradeRisk.CRITICAL]])
if high_risk_count > 0:
strategies.append("Create comprehensive test suite before high-risk upgrades")
strategies.append("Plan rollback procedures for critical upgrades")
major_count = len([u for u in upgrades if u.update_type == UpdateType.MAJOR])
if major_count > 3:
strategies.append("Phase major upgrades across multiple releases")
strategies.append("Use feature flags for gradual rollout")
security_count = len([u for u in upgrades if u.security_updates])
if security_count > 0:
strategies.append("Prioritize security updates regardless of risk level")
return strategies
def _create_upgrade_plans(self, upgrades: List[DependencyUpgrade], timeline_days: int) -> List[UpgradePlan]:
"""Create phased upgrade plans."""
if not upgrades:
return []
# Sort upgrades by priority score (descending)
sorted_upgrades = sorted(upgrades, key=lambda x: x.priority_score, reverse=True)
plans = []
# Phase 1: Security and safe updates (first 30% of timeline)
phase1_upgrades = [u for u in sorted_upgrades if
u.security_updates or u.risk_level == UpgradeRisk.SAFE][:10]
if phase1_upgrades:
plans.append(self._create_upgrade_plan(
"Phase 1: Security & Safe Updates",
"Immediate security fixes and low-risk updates",
1, phase1_upgrades, timeline_days // 3
))
# Phase 2: Low-medium risk updates (middle 40% of timeline)
phase2_upgrades = [u for u in sorted_upgrades if
u.risk_level in [UpgradeRisk.LOW, UpgradeRisk.MEDIUM] and
not u.security_updates][:8]
if phase2_upgrades:
plans.append(self._create_upgrade_plan(
"Phase 2: Regular Updates",
"Standard dependency updates with moderate risk",
2, phase2_upgrades, timeline_days * 2 // 5
))
# Phase 3: High-risk and major updates (final 30% of timeline)
phase3_upgrades = [u for u in sorted_upgrades if
u.risk_level in [UpgradeRisk.HIGH, UpgradeRisk.CRITICAL]][:5]
if phase3_upgrades:
plans.append(self._create_upgrade_plan(
"Phase 3: Major Updates",
"High-risk upgrades requiring careful planning",
3, phase3_upgrades, timeline_days // 3
))
return plans
def _create_upgrade_plan(self, name: str, description: str, phase: int,
upgrades: List[DependencyUpgrade], duration_days: int) -> UpgradePlan:
"""Create a detailed upgrade plan for a phase."""
dependency_names = [u.name for u in upgrades]
# Generate migration steps
migration_steps = []
migration_steps.append("1. Create feature branch for upgrades")
migration_steps.append("2. Update dependency versions in manifest files")
migration_steps.append("3. Run dependency install/update commands")
migration_steps.append("4. Fix breaking changes and deprecation warnings")
migration_steps.append("5. Update test suite for compatibility")
migration_steps.append("6. Run comprehensive test suite")
migration_steps.append("7. Update documentation and changelog")
migration_steps.append("8. Create pull request for review")
# Add phase-specific steps
if phase == 1:
migration_steps.insert(3, "3a. Verify security fixes are applied")
elif phase == 3:
migration_steps.insert(5, "5a. Perform extensive integration testing")
migration_steps.insert(6, "6a. Test with production-like data")
# Generate testing requirements
testing_requirements = [
"Unit test suite passes 100%",
"Integration tests cover upgrade scenarios",
"Performance benchmarks within acceptable range"
]
if any(u.risk_level in [UpgradeRisk.HIGH, UpgradeRisk.CRITICAL] for u in upgrades):
testing_requirements.extend([
"Manual testing of critical user flows",
"Load testing for performance regression",
"Security scanning for new vulnerabilities"
])
# Generate rollback plan
rollback_plan = [
"1. Revert dependency versions in manifest files",
"2. Run dependency install with previous versions",
"3. Restore previous configuration files if changed",
"4. Run smoke tests to verify rollback success",
"5. Monitor system health metrics"
]
# Success criteria
success_criteria = [
"All tests pass in CI/CD pipeline",
"No security vulnerabilities introduced",
"Performance metrics within acceptable thresholds",
"No critical user workflows broken"
]
return UpgradePlan(
name=name,
description=description,
phase=phase,
dependencies=dependency_names,
estimated_duration=f"{duration_days} days",
prerequisites=self._generate_prerequisites(upgrades),
migration_steps=migration_steps,
testing_requirements=testing_requirements,
rollback_plan=rollback_plan,
success_criteria=success_criteria
)
def _generate_prerequisites(self, upgrades: List[DependencyUpgrade]) -> List[str]:
"""Generate prerequisites for upgrade phase."""
prerequisites = [
"Comprehensive test suite with good coverage",
"Backup of current working state",
"Development environment setup"
]
if any(u.risk_level in [UpgradeRisk.HIGH, UpgradeRisk.CRITICAL] for u in upgrades):
prerequisites.extend([
"Staging environment for testing",
"Rollback procedure documented and tested",
"Team availability for issue resolution"
])
if any(u.security_updates for u in upgrades):
prerequisites.append("Security team notification for validation")
return prerequisites
def _generate_upgrade_recommendations(self, analysis_results: Dict[str, Any]) -> List[str]:
"""Generate actionable upgrade recommendations."""
recommendations = []
security_count = analysis_results['upgrade_statistics'].get('security_updates', 0)
if security_count > 0:
recommendations.append(f"URGENT: {security_count} security updates available - prioritize immediately")
safe_count = analysis_results['upgrade_statistics']['by_risk'].get('safe', 0)
if safe_count > 0:
recommendations.append(f"Quick wins: {safe_count} safe updates can be applied with minimal risk")
critical_count = analysis_results['risk_assessment']['high_risk_count']
if critical_count > 0:
recommendations.append(f"Plan carefully: {critical_count} high-risk upgrades need thorough testing")
major_count = analysis_results['upgrade_statistics']['by_type'].get('major', 0)
if major_count > 3:
recommendations.append("Consider phasing major upgrades across multiple releases")
overall_risk = analysis_results['risk_assessment']['overall_risk']
if overall_risk in ['HIGH', 'CRITICAL']:
recommendations.append("Overall upgrade risk is high - recommend gradual approach")
return recommendations
def generate_report(self, analysis_results: Dict[str, Any], format: str = 'text') -> str:
"""Generate upgrade plan report in specified format."""
if format == 'json':
# Convert dataclass objects for JSON serialization
serializable_results = analysis_results.copy()
serializable_results['available_upgrades'] = [asdict(upgrade) for upgrade in analysis_results['available_upgrades']]
serializable_results['upgrade_plans'] = [asdict(plan) for plan in analysis_results['upgrade_plans']]
return json.dumps(serializable_results, indent=2, default=str)
# Text format report
report = []
report.append("=" * 60)
report.append("DEPENDENCY UPGRADE PLAN")
report.append("=" * 60)
report.append(f"Generated: {analysis_results['timestamp']}")
report.append(f"Timeline: {analysis_results['timeline_days']} days")
report.append("")
# Statistics
stats = analysis_results['upgrade_statistics']
report.append("UPGRADE SUMMARY:")
report.append(f" Total Upgrades Available: {stats.get('total_upgrades', 0)}")
report.append(f" Security Updates: {stats.get('security_updates', 0)}")
report.append(f" Major Version Updates: {stats['by_type'].get('major', 0)}")
report.append(f" High Risk Updates: {stats['by_risk'].get('high', 0)}")
report.append("")
# Risk Assessment
risk = analysis_results['risk_assessment']
report.append("RISK ASSESSMENT:")
report.append(f" Overall Risk Level: {risk['overall_risk']}")
if risk.get('risk_factors'):
report.append(" Key Risk Factors:")
for factor in risk['risk_factors'][:3]:
report.append(f" • {factor}")
report.append("")
# High Priority Upgrades
high_priority = sorted([u for u in analysis_results['available_upgrades']],
key=lambda x: x.priority_score, reverse=True)[:10]
if high_priority:
report.append("TOP PRIORITY UPGRADES:")
report.append("-" * 30)
for upgrade in high_priority:
risk_indicator = "🔴" if upgrade.risk_level in [UpgradeRisk.HIGH, UpgradeRisk.CRITICAL] else \
"🟡" if upgrade.risk_level == UpgradeRisk.MEDIUM else "🟢"
security_indicator = " 🔒" if upgrade.security_updates else ""
report.append(f"{risk_indicator} {upgrade.name}: {upgrade.current_version} → {upgrade.latest_version}{security_indicator}")
report.append(f" Type: {upgrade.update_type.value.title()} | Risk: {upgrade.risk_level.value.title()} | Priority: {upgrade.priority_score:.1f}")
if upgrade.security_updates:
report.append(f" Security: {upgrade.security_updates[0]}")
report.append("")
# Upgrade Plans
if analysis_results['upgrade_plans']:
report.append("PHASED UPGRADE PLANS:")
report.append("-" * 30)
for plan in analysis_results['upgrade_plans']:
report.append(f"{plan.name} ({plan.estimated_duration})")
report.append(f" Dependencies: {', '.join(plan.dependencies[:5])}")
if len(plan.dependencies) > 5:
report.append(f" ... and {len(plan.dependencies) - 5} more")
report.append(f" Key Steps: {'; '.join(plan.migration_steps[:3])}")
report.append("")
# Recommendations
if analysis_results['recommendations']:
report.append("RECOMMENDATIONS:")
report.append("-" * 20)
for i, rec in enumerate(analysis_results['recommendations'], 1):
report.append(f"{i}. {rec}")
report.append("")
report.append("=" * 60)
return '\n'.join(report)
def main():
"""Main entry point for the upgrade planner."""
parser = argparse.ArgumentParser(
description='Analyze dependency upgrades and create migration plans',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python upgrade_planner.py deps.json
python upgrade_planner.py inventory.json --timeline 60 --format json
python upgrade_planner.py deps.json --risk-threshold medium --output plan.txt
"""
)
parser.add_argument('inventory_file',
help='Path to dependency inventory JSON file')
parser.add_argument('--timeline', type=int, default=90,
help='Timeline for upgrade plan in days (default: 90)')
parser.add_argument('--format', choices=['text', 'json'], default='text',
help='Output format (default: text)')
parser.add_argument('--output', '-o',
help='Output file path (default: stdout)')
parser.add_argument('--risk-threshold',
choices=['safe', 'low', 'medium', 'high', 'critical'],
default='high',
help='Maximum risk level to include (default: high)')
parser.add_argument('--security-only', action='store_true',
help='Only plan upgrades with security fixes')
args = parser.parse_args()
try:
planner = UpgradePlanner()
results = planner.analyze_upgrades(args.inventory_file, args.timeline)
# Filter by risk threshold if specified
if args.risk_threshold != 'critical':
risk_levels = ['safe', 'low', 'medium', 'high', 'critical']
max_index = risk_levels.index(args.risk_threshold)
allowed_risks = set(risk_levels[:max_index + 1])
results['available_upgrades'] = [
u for u in results['available_upgrades']
if u.risk_level.value in allowed_risks
]
# Filter for security-only if specified
if args.security_only:
results['available_upgrades'] = [
u for u in results['available_upgrades']
if u.security_updates
]
report = planner.generate_report(results, args.format)
if args.output:
with open(args.output, 'w') as f:
f.write(report)
print(f"Upgrade plan saved to {args.output}")
else:
print(report)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
FILE:test-inventory.json
{
"timestamp": "2026-02-16T15:42:09.730696",
"project_path": "test-project",
"dependencies": [
{
"name": "express",
"version": "4.18.1",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": [
{
"id": "CVE-2022-24999",
"summary": "Open redirect in express",
"severity": "MEDIUM",
"cvss_score": 6.1,
"affected_versions": "<4.18.2",
"fixed_version": "4.18.2",
"published_date": "2022-11-26",
"references": [
"https://nvd.nist.gov/vuln/detail/CVE-2022-24999"
]
},
{
"id": "CVE-2022-24999",
"summary": "Open redirect in express",
"severity": "MEDIUM",
"cvss_score": 6.1,
"affected_versions": "<4.18.2",
"fixed_version": "4.18.2",
"published_date": "2022-11-26",
"references": [
"https://nvd.nist.gov/vuln/detail/CVE-2022-24999"
]
}
]
},
{
"name": "lodash",
"version": "4.17.20",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": [
{
"id": "CVE-2021-23337",
"summary": "Prototype pollution in lodash",
"severity": "HIGH",
"cvss_score": 7.2,
"affected_versions": "<4.17.21",
"fixed_version": "4.17.21",
"published_date": "2021-02-15",
"references": [
"https://nvd.nist.gov/vuln/detail/CVE-2021-23337"
]
},
{
"id": "CVE-2021-23337",
"summary": "Prototype pollution in lodash",
"severity": "HIGH",
"cvss_score": 7.2,
"affected_versions": "<4.17.21",
"fixed_version": "4.17.21",
"published_date": "2021-02-15",
"references": [
"https://nvd.nist.gov/vuln/detail/CVE-2021-23337"
]
}
]
},
{
"name": "axios",
"version": "1.5.0",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": [
{
"id": "CVE-2023-45857",
"summary": "Cross-site request forgery in axios",
"severity": "MEDIUM",
"cvss_score": 6.1,
"affected_versions": ">=1.0.0 <1.6.0",
"fixed_version": "1.6.0",
"published_date": "2023-10-11",
"references": [
"https://nvd.nist.gov/vuln/detail/CVE-2023-45857"
]
},
{
"id": "CVE-2023-45857",
"summary": "Cross-site request forgery in axios",
"severity": "MEDIUM",
"cvss_score": 6.1,
"affected_versions": ">=1.0.0 <1.6.0",
"fixed_version": "1.6.0",
"published_date": "2023-10-11",
"references": [
"https://nvd.nist.gov/vuln/detail/CVE-2023-45857"
]
}
]
},
{
"name": "jsonwebtoken",
"version": "8.5.1",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "bcrypt",
"version": "5.1.0",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "mongoose",
"version": "6.10.0",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "cors",
"version": "2.8.5",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "helmet",
"version": "6.1.5",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "winston",
"version": "3.8.2",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "dotenv",
"version": "16.0.3",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "express-rate-limit",
"version": "6.7.0",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "multer",
"version": "1.4.5-lts.1",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "sharp",
"version": "0.32.1",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "nodemailer",
"version": "6.9.1",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "socket.io",
"version": "4.6.1",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "redis",
"version": "4.6.5",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "moment",
"version": "2.29.4",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "chalk",
"version": "4.1.2",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "commander",
"version": "9.4.1",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "nodemon",
"version": "2.0.22",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "jest",
"version": "29.5.0",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "supertest",
"version": "6.3.3",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "eslint",
"version": "8.40.0",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "eslint-config-airbnb-base",
"version": "15.0.0",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "eslint-plugin-import",
"version": "2.27.5",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "webpack",
"version": "5.82.1",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "webpack-cli",
"version": "5.1.1",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "babel-loader",
"version": "9.1.2",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "@babel/core",
"version": "7.22.1",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "@babel/preset-env",
"version": "7.22.2",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "css-loader",
"version": "6.7.4",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "style-loader",
"version": "3.3.3",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "html-webpack-plugin",
"version": "5.5.1",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "mini-css-extract-plugin",
"version": "2.7.6",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "postcss",
"version": "8.4.23",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "postcss-loader",
"version": "7.3.0",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "autoprefixer",
"version": "10.4.14",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "cross-env",
"version": "7.0.3",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
},
{
"name": "rimraf",
"version": "5.0.1",
"ecosystem": "npm",
"direct": true,
"license": null,
"vulnerabilities": []
}
],
"vulnerabilities_found": 6,
"high_severity_count": 2,
"medium_severity_count": 4,
"low_severity_count": 0,
"ecosystems": [
"npm"
],
"scan_summary": {
"total_dependencies": 39,
"unique_dependencies": 39,
"ecosystems_found": 1,
"vulnerable_dependencies": 3,
"vulnerability_breakdown": {
"high": 2,
"medium": 4,
"low": 0
}
},
"recommendations": [
"URGENT: Address 2 high-severity vulnerabilities immediately",
"Schedule fixes for 4 medium-severity vulnerabilities within 30 days",
"Update express from 4.18.1 to 4.18.2 to fix CVE-2022-24999",
"Update express from 4.18.1 to 4.18.2 to fix CVE-2022-24999",
"Update lodash from 4.17.20 to 4.17.21 to fix CVE-2021-23337",
"Update lodash from 4.17.20 to 4.17.21 to fix CVE-2021-23337",
"Update axios from 1.5.0 to 1.6.0 to fix CVE-2023-45857",
"Update axios from 1.5.0 to 1.6.0 to fix CVE-2023-45857"
]
}
FILE:test-project/package.json
{
"name": "sample-web-app",
"version": "1.2.3",
"description": "A sample web application with various dependencies for testing dependency auditing",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"build": "webpack --mode production",
"test": "jest",
"lint": "eslint src/",
"audit": "npm audit"
},
"keywords": ["web", "app", "sample", "dependency", "audit"],
"author": "Claude Skills Team",
"license": "MIT",
"dependencies": {
"express": "4.18.1",
"lodash": "4.17.20",
"axios": "1.5.0",
"jsonwebtoken": "8.5.1",
"bcrypt": "5.1.0",
"mongoose": "6.10.0",
"cors": "2.8.5",
"helmet": "6.1.5",
"winston": "3.8.2",
"dotenv": "16.0.3",
"express-rate-limit": "6.7.0",
"multer": "1.4.5-lts.1",
"sharp": "0.32.1",
"nodemailer": "6.9.1",
"socket.io": "4.6.1",
"redis": "4.6.5",
"moment": "2.29.4",
"chalk": "4.1.2",
"commander": "9.4.1"
},
"devDependencies": {
"nodemon": "2.0.22",
"jest": "29.5.0",
"supertest": "6.3.3",
"eslint": "8.40.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-plugin-import": "2.27.5",
"webpack": "5.82.1",
"webpack-cli": "5.1.1",
"babel-loader": "9.1.2",
"@babel/core": "7.22.1",
"@babel/preset-env": "7.22.2",
"css-loader": "6.7.4",
"style-loader": "3.3.3",
"html-webpack-plugin": "5.5.1",
"mini-css-extract-plugin": "2.7.6",
"postcss": "8.4.23",
"postcss-loader": "7.3.0",
"autoprefixer": "10.4.14",
"cross-env": "7.0.3",
"rimraf": "5.0.1"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/example/sample-web-app.git"
},
"bugs": {
"url": "https://github.com/example/sample-web-app/issues"
},
"homepage": "https://github.com/example/sample-web-app#readme"
}Observability Designer (POWERFUL)
---
name: "observability-designer"
description: "Observability Designer (POWERFUL)"
---
# Observability Designer (POWERFUL)
**Category:** Engineering
**Tier:** POWERFUL
**Description:** Design comprehensive observability strategies for production systems including SLI/SLO frameworks, alerting optimization, and dashboard generation.
## Overview
Observability Designer enables you to create production-ready observability strategies that provide deep insights into system behavior, performance, and reliability. This skill combines the three pillars of observability (metrics, logs, traces) with proven frameworks like SLI/SLO design, golden signals monitoring, and alert optimization to create comprehensive observability solutions.
## Core Competencies
### SLI/SLO/SLA Framework Design
- **Service Level Indicators (SLI):** Define measurable signals that indicate service health
- **Service Level Objectives (SLO):** Set reliability targets based on user experience
- **Service Level Agreements (SLA):** Establish customer-facing commitments with consequences
- **Error Budget Management:** Calculate and track error budget consumption
- **Burn Rate Alerting:** Multi-window burn rate alerts for proactive SLO protection
### Three Pillars of Observability
#### Metrics
- **Golden Signals:** Latency, traffic, errors, and saturation monitoring
- **RED Method:** Rate, Errors, and Duration for request-driven services
- **USE Method:** Utilization, Saturation, and Errors for resource monitoring
- **Business Metrics:** Revenue, user engagement, and feature adoption tracking
- **Infrastructure Metrics:** CPU, memory, disk, network, and custom resource metrics
#### Logs
- **Structured Logging:** JSON-based log formats with consistent fields
- **Log Aggregation:** Centralized log collection and indexing strategies
- **Log Levels:** Appropriate use of DEBUG, INFO, WARN, ERROR, FATAL levels
- **Correlation IDs:** Request tracing through distributed systems
- **Log Sampling:** Volume management for high-throughput systems
#### Traces
- **Distributed Tracing:** End-to-end request flow visualization
- **Span Design:** Meaningful span boundaries and metadata
- **Trace Sampling:** Intelligent sampling strategies for performance and cost
- **Service Maps:** Automatic dependency discovery through traces
- **Root Cause Analysis:** Trace-driven debugging workflows
### Dashboard Design Principles
#### Information Architecture
- **Hierarchy:** Overview → Service → Component → Instance drill-down paths
- **Golden Ratio:** 80% operational metrics, 20% exploratory metrics
- **Cognitive Load:** Maximum 7±2 panels per dashboard screen
- **User Journey:** Role-based dashboard personas (SRE, Developer, Executive)
#### Visualization Best Practices
- **Chart Selection:** Time series for trends, heatmaps for distributions, gauges for status
- **Color Theory:** Red for critical, amber for warning, green for healthy states
- **Reference Lines:** SLO targets, capacity thresholds, and historical baselines
- **Time Ranges:** Default to meaningful windows (4h for incidents, 7d for trends)
#### Panel Design
- **Metric Queries:** Efficient Prometheus/InfluxDB queries with proper aggregation
- **Alerting Integration:** Visual alert state indicators on relevant panels
- **Interactive Elements:** Template variables, drill-down links, and annotation overlays
- **Performance:** Sub-second render times through query optimization
### Alert Design and Optimization
#### Alert Classification
- **Severity Levels:**
- **Critical:** Service down, SLO burn rate high
- **Warning:** Approaching thresholds, non-user-facing issues
- **Info:** Deployment notifications, capacity planning alerts
- **Actionability:** Every alert must have a clear response action
- **Alert Routing:** Escalation policies based on severity and team ownership
#### Alert Fatigue Prevention
- **Signal vs Noise:** High precision (few false positives) over high recall
- **Hysteresis:** Different thresholds for firing and resolving alerts
- **Suppression:** Dependent alert suppression during known outages
- **Grouping:** Related alerts grouped into single notifications
#### Alert Rule Design
- **Threshold Selection:** Statistical methods for threshold determination
- **Window Functions:** Appropriate averaging windows and percentile calculations
- **Alert Lifecycle:** Clear firing conditions and automatic resolution criteria
- **Testing:** Alert rule validation against historical data
### Runbook Generation and Incident Response
#### Runbook Structure
- **Alert Context:** What the alert means and why it fired
- **Impact Assessment:** User-facing vs internal impact evaluation
- **Investigation Steps:** Ordered troubleshooting procedures with time estimates
- **Resolution Actions:** Common fixes and escalation procedures
- **Post-Incident:** Follow-up tasks and prevention measures
#### Incident Detection Patterns
- **Anomaly Detection:** Statistical methods for detecting unusual patterns
- **Composite Alerts:** Multi-signal alerts for complex failure modes
- **Predictive Alerts:** Capacity and trend-based forward-looking alerts
- **Canary Monitoring:** Early detection through progressive deployment monitoring
### Golden Signals Framework
#### Latency Monitoring
- **Request Latency:** P50, P95, P99 response time tracking
- **Queue Latency:** Time spent waiting in processing queues
- **Network Latency:** Inter-service communication delays
- **Database Latency:** Query execution and connection pool metrics
#### Traffic Monitoring
- **Request Rate:** Requests per second with burst detection
- **Bandwidth Usage:** Network throughput and capacity utilization
- **User Sessions:** Active user tracking and session duration
- **Feature Usage:** API endpoint and feature adoption metrics
#### Error Monitoring
- **Error Rate:** 4xx and 5xx HTTP response code tracking
- **Error Budget:** SLO-based error rate targets and consumption
- **Error Distribution:** Error type classification and trending
- **Silent Failures:** Detection of processing failures without HTTP errors
#### Saturation Monitoring
- **Resource Utilization:** CPU, memory, disk, and network usage
- **Queue Depth:** Processing queue length and wait times
- **Connection Pools:** Database and service connection saturation
- **Rate Limiting:** API throttling and quota exhaustion tracking
### Distributed Tracing Strategies
#### Trace Architecture
- **Sampling Strategy:** Head-based, tail-based, and adaptive sampling
- **Trace Propagation:** Context propagation across service boundaries
- **Span Correlation:** Parent-child relationship modeling
- **Trace Storage:** Retention policies and storage optimization
#### Service Instrumentation
- **Auto-Instrumentation:** Framework-based automatic trace generation
- **Manual Instrumentation:** Custom span creation for business logic
- **Baggage Handling:** Cross-cutting concern propagation
- **Performance Impact:** Instrumentation overhead measurement and optimization
### Log Aggregation Patterns
#### Collection Architecture
- **Agent Deployment:** Log shipping agent strategies (push vs pull)
- **Log Routing:** Topic-based routing and filtering
- **Parsing Strategies:** Structured vs unstructured log handling
- **Schema Evolution:** Log format versioning and migration
#### Storage and Indexing
- **Index Design:** Optimized field indexing for common query patterns
- **Retention Policies:** Time and volume-based log retention
- **Compression:** Log data compression and archival strategies
- **Search Performance:** Query optimization and result caching
### Cost Optimization for Observability
#### Data Management
- **Metric Retention:** Tiered retention based on metric importance
- **Log Sampling:** Intelligent sampling to reduce ingestion costs
- **Trace Sampling:** Cost-effective trace collection strategies
- **Data Archival:** Cold storage for historical observability data
#### Resource Optimization
- **Query Efficiency:** Optimized metric and log queries
- **Storage Costs:** Appropriate storage tiers for different data types
- **Ingestion Rate Limiting:** Controlled data ingestion to manage costs
- **Cardinality Management:** High-cardinality metric detection and mitigation
## Scripts Overview
This skill includes three powerful Python scripts for comprehensive observability design:
### 1. SLO Designer (`slo_designer.py`)
Generates complete SLI/SLO frameworks based on service characteristics:
- **Input:** Service description JSON (type, criticality, dependencies)
- **Output:** SLI definitions, SLO targets, error budgets, burn rate alerts, SLA recommendations
- **Features:** Multi-window burn rate calculations, error budget policies, alert rule generation
### 2. Alert Optimizer (`alert_optimizer.py`)
Analyzes and optimizes existing alert configurations:
- **Input:** Alert configuration JSON with rules, thresholds, and routing
- **Output:** Optimization report and improved alert configuration
- **Features:** Noise detection, coverage gaps, duplicate identification, threshold optimization
### 3. Dashboard Generator (`dashboard_generator.py`)
Creates comprehensive dashboard specifications:
- **Input:** Service/system description JSON
- **Output:** Grafana-compatible dashboard JSON and documentation
- **Features:** Golden signals coverage, RED/USE methods, drill-down paths, role-based views
## Integration Patterns
### Monitoring Stack Integration
- **Prometheus:** Metric collection and alerting rule generation
- **Grafana:** Dashboard creation and visualization configuration
- **Elasticsearch/Kibana:** Log analysis and dashboard integration
- **Jaeger/Zipkin:** Distributed tracing configuration and analysis
### CI/CD Integration
- **Pipeline Monitoring:** Build, test, and deployment observability
- **Deployment Correlation:** Release impact tracking and rollback triggers
- **Feature Flag Monitoring:** A/B test and feature rollout observability
- **Performance Regression:** Automated performance monitoring in pipelines
### Incident Management Integration
- **PagerDuty/VictorOps:** Alert routing and escalation policies
- **Slack/Teams:** Notification and collaboration integration
- **JIRA/ServiceNow:** Incident tracking and resolution workflows
- **Post-Mortem:** Automated incident analysis and improvement tracking
## Advanced Patterns
### Multi-Cloud Observability
- **Cross-Cloud Metrics:** Unified metrics across AWS, GCP, Azure
- **Network Observability:** Inter-cloud connectivity monitoring
- **Cost Attribution:** Cloud resource cost tracking and optimization
- **Compliance Monitoring:** Security and compliance posture tracking
### Microservices Observability
- **Service Mesh Integration:** Istio/Linkerd observability configuration
- **API Gateway Monitoring:** Request routing and rate limiting observability
- **Container Orchestration:** Kubernetes cluster and workload monitoring
- **Service Discovery:** Dynamic service monitoring and health checks
### Machine Learning Observability
- **Model Performance:** Accuracy, drift, and bias monitoring
- **Feature Store Monitoring:** Feature quality and freshness tracking
- **Pipeline Observability:** ML pipeline execution and performance monitoring
- **A/B Test Analysis:** Statistical significance and business impact measurement
## Best Practices
### Organizational Alignment
- **SLO Setting:** Collaborative target setting between product and engineering
- **Alert Ownership:** Clear escalation paths and team responsibilities
- **Dashboard Governance:** Centralized dashboard management and standards
- **Training Programs:** Team education on observability tools and practices
### Technical Excellence
- **Infrastructure as Code:** Observability configuration version control
- **Testing Strategy:** Alert rule testing and dashboard validation
- **Performance Monitoring:** Observability system performance tracking
- **Security Considerations:** Access control and data privacy in observability
### Continuous Improvement
- **Metrics Review:** Regular SLI/SLO effectiveness assessment
- **Alert Tuning:** Ongoing alert threshold and routing optimization
- **Dashboard Evolution:** User feedback-driven dashboard improvements
- **Tool Evaluation:** Regular assessment of observability tool effectiveness
## Success Metrics
### Operational Metrics
- **Mean Time to Detection (MTTD):** How quickly issues are identified
- **Mean Time to Resolution (MTTR):** Time from detection to resolution
- **Alert Precision:** Percentage of actionable alerts
- **SLO Achievement:** Percentage of SLO targets met consistently
### Business Metrics
- **System Reliability:** Overall uptime and user experience quality
- **Engineering Velocity:** Development team productivity and deployment frequency
- **Cost Efficiency:** Observability cost as percentage of infrastructure spend
- **Customer Satisfaction:** User-reported reliability and performance satisfaction
This comprehensive observability design skill enables organizations to build robust, scalable monitoring and alerting systems that provide actionable insights while maintaining cost efficiency and operational excellence.
FILE:README.md
# Observability Designer
A comprehensive toolkit for designing production-ready observability strategies including SLI/SLO frameworks, alert optimization, and dashboard generation.
## Overview
The Observability Designer skill provides three powerful Python scripts that help you create, optimize, and maintain observability systems:
- **SLO Designer**: Generate complete SLI/SLO frameworks with error budgets and burn rate alerts
- **Alert Optimizer**: Analyze and optimize existing alert configurations to reduce noise and improve effectiveness
- **Dashboard Generator**: Create comprehensive dashboard specifications with role-based layouts and drill-down paths
## Quick Start
### Prerequisites
- Python 3.7+
- No external dependencies required (uses Python standard library only)
### Basic Usage
```bash
# Generate SLO framework for a service
python3 scripts/slo_designer.py --service-type api --criticality critical --user-facing true --service-name payment-service
# Optimize existing alerts
python3 scripts/alert_optimizer.py --input assets/sample_alerts.json --analyze-only
# Generate a dashboard specification
python3 scripts/dashboard_generator.py --service-type web --name "Customer Portal" --role sre
```
## Scripts Documentation
### SLO Designer (`slo_designer.py`)
Generates comprehensive SLO frameworks based on service characteristics.
#### Features
- **Automatic SLI Selection**: Recommends appropriate SLIs based on service type
- **Target Setting**: Suggests SLO targets based on service criticality
- **Error Budget Calculation**: Computes error budgets and burn rate thresholds
- **Multi-Window Burn Rate Alerts**: Generates 4-window burn rate alerting rules
- **SLA Recommendations**: Provides customer-facing SLA guidance
#### Usage Examples
```bash
# From service definition file
python3 scripts/slo_designer.py --input assets/sample_service_api.json --output slo_framework.json
# From command line parameters
python3 scripts/slo_designer.py \
--service-type api \
--criticality critical \
--user-facing true \
--service-name payment-service \
--output payment_slos.json
# Generate and display summary only
python3 scripts/slo_designer.py --input assets/sample_service_web.json --summary-only
```
#### Service Definition Format
```json
{
"name": "payment-service",
"type": "api",
"criticality": "critical",
"user_facing": true,
"description": "Handles payment processing",
"team": "payments",
"environment": "production",
"dependencies": [
{
"name": "user-service",
"type": "api",
"criticality": "high"
}
]
}
```
#### Supported Service Types
- **api**: REST APIs, GraphQL services
- **web**: Web applications, SPAs
- **database**: Database services, data stores
- **queue**: Message queues, event streams
- **batch**: Batch processing jobs
- **ml**: Machine learning services
#### Criticality Levels
- **critical**: 99.99% availability, <100ms P95 latency, <0.1% error rate
- **high**: 99.9% availability, <200ms P95 latency, <0.5% error rate
- **medium**: 99.5% availability, <500ms P95 latency, <1% error rate
- **low**: 99% availability, <1s P95 latency, <2% error rate
### Alert Optimizer (`alert_optimizer.py`)
Analyzes existing alert configurations and provides optimization recommendations.
#### Features
- **Noise Detection**: Identifies alerts with high false positive rates
- **Coverage Analysis**: Finds gaps in monitoring coverage
- **Duplicate Detection**: Locates redundant or overlapping alerts
- **Threshold Analysis**: Reviews alert thresholds for appropriateness
- **Fatigue Assessment**: Evaluates alert volume and routing
#### Usage Examples
```bash
# Analyze existing alerts
python3 scripts/alert_optimizer.py --input assets/sample_alerts.json --analyze-only
# Generate optimized configuration
python3 scripts/alert_optimizer.py \
--input assets/sample_alerts.json \
--output optimized_alerts.json
# Generate HTML report
python3 scripts/alert_optimizer.py \
--input assets/sample_alerts.json \
--report alert_analysis.html \
--format html
```
#### Alert Configuration Format
```json
{
"alerts": [
{
"alert": "HighLatency",
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 0.5",
"for": "5m",
"labels": {
"severity": "warning",
"service": "payment-service"
},
"annotations": {
"summary": "High request latency detected",
"runbook_url": "https://runbooks.company.com/high-latency"
},
"historical_data": {
"fires_per_day": 2.5,
"false_positive_rate": 0.15
}
}
],
"services": [
{
"name": "payment-service",
"criticality": "critical"
}
]
}
```
#### Analysis Categories
- **Golden Signals**: Latency, traffic, errors, saturation
- **Resource Utilization**: CPU, memory, disk, network
- **Business Metrics**: Revenue, conversion, user engagement
- **Security**: Auth failures, suspicious activity
- **Availability**: Uptime, health checks
### Dashboard Generator (`dashboard_generator.py`)
Creates comprehensive dashboard specifications with role-based optimization.
#### Features
- **Role-Based Layouts**: Optimized for SRE, Developer, Executive, and Ops personas
- **Golden Signals Coverage**: Automatic inclusion of key monitoring metrics
- **Service-Type Specific Panels**: Tailored panels based on service characteristics
- **Interactive Elements**: Template variables, drill-down paths, time range controls
- **Grafana Compatibility**: Generates Grafana-compatible JSON
#### Usage Examples
```bash
# From service definition
python3 scripts/dashboard_generator.py \
--input assets/sample_service_web.json \
--output dashboard.json
# With specific role optimization
python3 scripts/dashboard_generator.py \
--service-type api \
--name "Payment Service" \
--role developer \
--output payment_dev_dashboard.json
# Generate Grafana-compatible JSON
python3 scripts/dashboard_generator.py \
--input assets/sample_service_api.json \
--output dashboard.json \
--format grafana
# With documentation
python3 scripts/dashboard_generator.py \
--service-type web \
--name "Customer Portal" \
--output portal_dashboard.json \
--doc-output portal_docs.md
```
#### Target Roles
- **sre**: Focus on availability, latency, errors, resource utilization
- **developer**: Emphasize latency, errors, throughput, business metrics
- **executive**: Highlight availability, business metrics, user experience
- **ops**: Priority on resource utilization, capacity, alerts, deployments
#### Panel Types
- **Stat**: Single value displays with thresholds
- **Gauge**: Resource utilization and capacity metrics
- **Timeseries**: Trend analysis and historical data
- **Table**: Top N lists and detailed breakdowns
- **Heatmap**: Distribution and correlation analysis
## Sample Data
The `assets/` directory contains sample configurations for testing:
- `sample_service_api.json`: Critical API service definition
- `sample_service_web.json`: High-priority web application definition
- `sample_alerts.json`: Alert configuration with optimization opportunities
The `expected_outputs/` directory shows example outputs from each script:
- `sample_slo_framework.json`: Complete SLO framework for API service
- `optimized_alerts.json`: Optimized alert configuration
- `sample_dashboard.json`: SRE dashboard specification
## Best Practices
### SLO Design
- Start with 1-2 SLOs per service and iterate
- Choose SLIs that directly impact user experience
- Set targets based on user needs, not technical capabilities
- Use error budgets to balance reliability and velocity
### Alert Optimization
- Every alert must be actionable
- Alert on symptoms, not causes
- Use multi-window burn rate alerts for SLO protection
- Implement proper escalation and routing policies
### Dashboard Design
- Follow the F-pattern for visual hierarchy
- Use consistent color semantics across dashboards
- Include drill-down paths for effective troubleshooting
- Optimize for the target role's specific needs
## Integration Patterns
### CI/CD Integration
```bash
# Generate SLOs during service onboarding
python3 scripts/slo_designer.py --input service-config.json --output slos.json
# Validate alert configurations in pipeline
python3 scripts/alert_optimizer.py --input alerts.json --analyze-only --report validation.html
# Auto-generate dashboards for new services
python3 scripts/dashboard_generator.py --input service-config.json --format grafana --output dashboard.json
```
### Monitoring Stack Integration
- **Prometheus**: Generated alert rules and recording rules
- **Grafana**: Dashboard JSON for direct import
- **Alertmanager**: Routing and escalation policies
- **PagerDuty**: Escalation configuration
### GitOps Workflow
1. Store service definitions in version control
2. Generate observability configurations in CI/CD
3. Deploy configurations via GitOps
4. Monitor effectiveness and iterate
## Advanced Usage
### Custom SLO Targets
Override default targets by including them in service definitions:
```json
{
"name": "special-service",
"type": "api",
"criticality": "high",
"custom_slos": {
"availability_target": 0.9995,
"latency_p95_target_ms": 150,
"error_rate_target": 0.002
}
}
```
### Alert Rule Templates
Use template variables for reusable alert rules:
```yaml
# Generated Prometheus alert rule
- alert: {{ service_name }}_HighLatency
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{service="{{ service_name }}"}[5m])) > {{ latency_threshold }}
for: 5m
labels:
severity: warning
service: "{{ service_name }}"
```
### Dashboard Variants
Generate multiple dashboard variants for different use cases:
```bash
# SRE operational dashboard
python3 scripts/dashboard_generator.py --input service.json --role sre --output sre-dashboard.json
# Developer debugging dashboard
python3 scripts/dashboard_generator.py --input service.json --role developer --output dev-dashboard.json
# Executive business dashboard
python3 scripts/dashboard_generator.py --input service.json --role executive --output exec-dashboard.json
```
## Troubleshooting
### Common Issues
#### Script Execution Errors
- Ensure Python 3.7+ is installed
- Check file paths and permissions
- Validate JSON syntax in input files
#### Invalid Service Definitions
- Required fields: `name`, `type`, `criticality`
- Valid service types: `api`, `web`, `database`, `queue`, `batch`, `ml`
- Valid criticality levels: `critical`, `high`, `medium`, `low`
#### Missing Historical Data
- Alert historical data is optional but improves analysis
- Include `fires_per_day` and `false_positive_rate` when available
- Use monitoring system APIs to populate historical metrics
### Debug Mode
Enable verbose logging by setting environment variable:
```bash
export DEBUG=1
python3 scripts/slo_designer.py --input service.json
```
## Contributing
### Development Setup
```bash
# Clone the repository
git clone <repository-url>
cd engineering/observability-designer
# Run tests
python3 -m pytest tests/
# Lint code
python3 -m flake8 scripts/
```
### Adding New Features
1. Follow existing code patterns and error handling
2. Include comprehensive docstrings and type hints
3. Add test cases for new functionality
4. Update documentation and examples
## Support
For questions, issues, or feature requests:
- Check existing documentation and examples
- Review the reference materials in `references/`
- Open an issue with detailed reproduction steps
- Include sample configurations when reporting bugs
---
*This skill is part of the Claude Skills marketplace. For more information about observability best practices, see the reference documentation in the `references/` directory.*
FILE:assets/sample_alerts.json
{
"alerts": [
{
"alert": "HighLatency",
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{service=\"payment-service\"}[5m])) > 0.5",
"for": "5m",
"labels": {
"severity": "warning",
"service": "payment-service",
"team": "payments"
},
"annotations": {
"summary": "High request latency detected",
"description": "95th percentile latency is {{ $value }}s for payment-service",
"runbook_url": "https://runbooks.company.com/high-latency"
},
"historical_data": {
"fires_per_day": 2.5,
"false_positive_rate": 0.15,
"average_duration_minutes": 12
}
},
{
"alert": "ServiceDown",
"expr": "up{service=\"payment-service\"} == 0",
"labels": {
"severity": "critical",
"service": "payment-service",
"team": "payments"
},
"annotations": {
"summary": "Payment service is down",
"description": "Payment service has been down for more than 1 minute",
"runbook_url": "https://runbooks.company.com/service-down"
},
"historical_data": {
"fires_per_day": 0.1,
"false_positive_rate": 0.05,
"average_duration_minutes": 3
}
},
{
"alert": "HighErrorRate",
"expr": "sum(rate(http_requests_total{service=\"payment-service\",code=~\"5..\"}[5m])) / sum(rate(http_requests_total{service=\"payment-service\"}[5m])) > 0.01",
"for": "2m",
"labels": {
"severity": "warning",
"service": "payment-service",
"team": "payments"
},
"annotations": {
"summary": "High error rate detected",
"description": "Error rate is {{ $value | humanizePercentage }} for payment-service",
"runbook_url": "https://runbooks.company.com/high-error-rate"
},
"historical_data": {
"fires_per_day": 1.8,
"false_positive_rate": 0.25,
"average_duration_minutes": 8
}
},
{
"alert": "HighCPUUsage",
"expr": "rate(process_cpu_seconds_total{service=\"payment-service\"}[5m]) * 100 > 80",
"labels": {
"severity": "warning",
"service": "payment-service",
"team": "payments"
},
"annotations": {
"summary": "High CPU usage",
"description": "CPU usage is {{ $value }}% for payment-service"
},
"historical_data": {
"fires_per_day": 15.2,
"false_positive_rate": 0.8,
"average_duration_minutes": 45
}
},
{
"alert": "HighMemoryUsage",
"expr": "process_resident_memory_bytes{service=\"payment-service\"} / process_virtual_memory_max_bytes{service=\"payment-service\"} * 100 > 85",
"labels": {
"severity": "info",
"service": "payment-service",
"team": "payments"
},
"annotations": {
"summary": "High memory usage",
"description": "Memory usage is {{ $value }}% for payment-service"
},
"historical_data": {
"fires_per_day": 8.5,
"false_positive_rate": 0.6,
"average_duration_minutes": 30
}
},
{
"alert": "DatabaseConnectionPoolExhaustion",
"expr": "db_connections_active{service=\"payment-service\"} / db_connections_max{service=\"payment-service\"} > 0.9",
"for": "1m",
"labels": {
"severity": "critical",
"service": "payment-service",
"team": "payments"
},
"annotations": {
"summary": "Database connection pool near exhaustion",
"description": "Connection pool utilization is {{ $value | humanizePercentage }}",
"runbook_url": "https://runbooks.company.com/db-connections"
},
"historical_data": {
"fires_per_day": 0.3,
"false_positive_rate": 0.1,
"average_duration_minutes": 5
}
},
{
"alert": "LowTraffic",
"expr": "sum(rate(http_requests_total{service=\"payment-service\"}[5m])) < 10",
"for": "10m",
"labels": {
"severity": "warning",
"service": "payment-service",
"team": "payments"
},
"annotations": {
"summary": "Unusually low traffic",
"description": "Request rate is {{ $value }} RPS, which is unusually low"
},
"historical_data": {
"fires_per_day": 12.0,
"false_positive_rate": 0.9,
"average_duration_minutes": 120
}
},
{
"alert": "HighLatencyDuplicate",
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{service=\"payment-service\"}[5m])) > 0.5",
"for": "5m",
"labels": {
"severity": "warning",
"service": "payment-service",
"team": "payments"
},
"annotations": {
"summary": "High request latency detected (duplicate)",
"description": "95th percentile latency is {{ $value }}s for payment-service"
},
"historical_data": {
"fires_per_day": 2.5,
"false_positive_rate": 0.15,
"average_duration_minutes": 12
}
},
{
"alert": "VeryLowErrorRate",
"expr": "sum(rate(http_requests_total{service=\"payment-service\",code=~\"5..\"}[5m])) / sum(rate(http_requests_total{service=\"payment-service\"}[5m])) > 0.001",
"labels": {
"severity": "info",
"service": "payment-service",
"team": "payments"
},
"annotations": {
"summary": "Error rate above 0.1%",
"description": "Error rate is {{ $value | humanizePercentage }}"
},
"historical_data": {
"fires_per_day": 25.0,
"false_positive_rate": 0.95,
"average_duration_minutes": 5
}
},
{
"alert": "DiskUsageHigh",
"expr": "disk_usage_percent{service=\"payment-service\"} > 85",
"labels": {
"severity": "warning",
"service": "payment-service",
"team": "payments"
},
"annotations": {
"summary": "Disk usage high",
"description": "Disk usage is {{ $value }}%"
},
"historical_data": {
"fires_per_day": 3.2,
"false_positive_rate": 0.4,
"average_duration_minutes": 240
}
}
],
"services": [
{
"name": "payment-service",
"type": "api",
"criticality": "critical",
"team": "payments"
},
{
"name": "user-service",
"type": "api",
"criticality": "high",
"team": "identity"
},
{
"name": "notification-service",
"type": "api",
"criticality": "medium",
"team": "communications"
}
],
"alert_routing": {
"routes": [
{
"match": {
"severity": "critical"
},
"receiver": "pager-critical",
"group_wait": "10s",
"group_interval": "1m",
"repeat_interval": "5m"
},
{
"match": {
"severity": "warning"
},
"receiver": "slack-warnings",
"group_wait": "30s",
"group_interval": "5m",
"repeat_interval": "1h"
},
{
"match": {
"severity": "info"
},
"receiver": "email-info",
"group_wait": "2m",
"group_interval": "10m",
"repeat_interval": "24h"
}
]
},
"receivers": [
{
"name": "pager-critical",
"pagerduty_configs": [
{
"routing_key": "pager-key-critical",
"description": "Critical alert: {{ range .Alerts }}{{ .Annotations.summary }}{{ end }}"
}
]
},
{
"name": "slack-warnings",
"slack_configs": [
{
"api_url": "https://hooks.slack.com/services/warnings",
"channel": "#alerts-warnings",
"title": "Warning Alert",
"text": "{{ range .Alerts }}{{ .Annotations.description }}{{ end }}"
}
]
},
{
"name": "email-info",
"email_configs": [
{
"to": "[email protected]",
"subject": "Info Alert: {{ .GroupLabels.alertname }}",
"body": "{{ range .Alerts }}{{ .Annotations.description }}{{ end }}"
}
]
}
]
}
FILE:assets/sample_service_api.json
{
"name": "payment-service",
"type": "api",
"criticality": "critical",
"user_facing": true,
"description": "Handles payment processing and transaction management",
"team": "payments",
"environment": "production",
"dependencies": [
{
"name": "user-service",
"type": "api",
"criticality": "high"
},
{
"name": "payment-gateway",
"type": "external",
"criticality": "critical"
},
{
"name": "fraud-detection",
"type": "ml",
"criticality": "high"
}
],
"endpoints": [
{
"path": "/api/v1/payments",
"method": "POST",
"sla_latency_ms": 500,
"expected_tps": 100
},
{
"path": "/api/v1/payments/{id}",
"method": "GET",
"sla_latency_ms": 200,
"expected_tps": 500
},
{
"path": "/api/v1/payments/{id}/refund",
"method": "POST",
"sla_latency_ms": 1000,
"expected_tps": 10
}
],
"business_metrics": {
"revenue_per_hour": {
"metric": "sum(payment_amount * rate(payments_successful_total[1h]))",
"target": 50000,
"unit": "USD"
},
"conversion_rate": {
"metric": "sum(rate(payments_successful_total[5m])) / sum(rate(payment_attempts_total[5m]))",
"target": 0.95,
"unit": "percentage"
}
},
"infrastructure": {
"container_orchestrator": "kubernetes",
"replicas": 6,
"cpu_limit": "2000m",
"memory_limit": "4Gi",
"database": {
"type": "postgresql",
"connection_pool_size": 20
},
"cache": {
"type": "redis",
"cluster_size": 3
}
},
"compliance_requirements": [
"PCI-DSS",
"SOX",
"GDPR"
],
"tags": [
"payment",
"transaction",
"critical-path",
"revenue-generating"
]
}
FILE:assets/sample_service_web.json
{
"name": "customer-portal",
"type": "web",
"criticality": "high",
"user_facing": true,
"description": "Customer-facing web application for account management and billing",
"team": "frontend",
"environment": "production",
"dependencies": [
{
"name": "user-service",
"type": "api",
"criticality": "high"
},
{
"name": "billing-service",
"type": "api",
"criticality": "high"
},
{
"name": "notification-service",
"type": "api",
"criticality": "medium"
},
{
"name": "cdn",
"type": "external",
"criticality": "medium"
}
],
"pages": [
{
"path": "/dashboard",
"sla_load_time_ms": 2000,
"expected_concurrent_users": 1000
},
{
"path": "/billing",
"sla_load_time_ms": 3000,
"expected_concurrent_users": 200
},
{
"path": "/settings",
"sla_load_time_ms": 1500,
"expected_concurrent_users": 100
}
],
"business_metrics": {
"daily_active_users": {
"metric": "count(user_sessions_started_total[1d])",
"target": 10000,
"unit": "users"
},
"session_duration": {
"metric": "avg(user_session_duration_seconds)",
"target": 300,
"unit": "seconds"
},
"bounce_rate": {
"metric": "sum(rate(page_views_bounced_total[1h])) / sum(rate(page_views_total[1h]))",
"target": 0.3,
"unit": "percentage"
}
},
"infrastructure": {
"container_orchestrator": "kubernetes",
"replicas": 4,
"cpu_limit": "1000m",
"memory_limit": "2Gi",
"storage": {
"type": "nfs",
"size": "50Gi"
},
"ingress": {
"type": "nginx",
"ssl_termination": true,
"rate_limiting": {
"requests_per_second": 100,
"burst": 200
}
}
},
"monitoring": {
"synthetic_checks": [
{
"name": "login_flow",
"url": "/auth/login",
"frequency": "1m",
"locations": ["us-east", "eu-west", "ap-south"]
},
{
"name": "checkout_flow",
"url": "/billing/checkout",
"frequency": "5m",
"locations": ["us-east", "eu-west"]
}
],
"rum": {
"enabled": true,
"sampling_rate": 0.1
}
},
"compliance_requirements": [
"GDPR",
"CCPA"
],
"tags": [
"frontend",
"customer-facing",
"billing",
"high-traffic"
]
}
FILE:expected_outputs/sample_dashboard.json
{
"metadata": {
"title": "customer-portal - SRE Dashboard",
"service": {
"name": "customer-portal",
"type": "web",
"criticality": "high",
"user_facing": true,
"description": "Customer-facing web application for account management and billing",
"team": "frontend",
"environment": "production",
"dependencies": [
{
"name": "user-service",
"type": "api",
"criticality": "high"
},
{
"name": "billing-service",
"type": "api",
"criticality": "high"
},
{
"name": "notification-service",
"type": "api",
"criticality": "medium"
},
{
"name": "cdn",
"type": "external",
"criticality": "medium"
}
],
"pages": [
{
"path": "/dashboard",
"sla_load_time_ms": 2000,
"expected_concurrent_users": 1000
},
{
"path": "/billing",
"sla_load_time_ms": 3000,
"expected_concurrent_users": 200
},
{
"path": "/settings",
"sla_load_time_ms": 1500,
"expected_concurrent_users": 100
}
],
"business_metrics": {
"daily_active_users": {
"metric": "count(user_sessions_started_total[1d])",
"target": 10000,
"unit": "users"
},
"session_duration": {
"metric": "avg(user_session_duration_seconds)",
"target": 300,
"unit": "seconds"
},
"bounce_rate": {
"metric": "sum(rate(page_views_bounced_total[1h])) / sum(rate(page_views_total[1h]))",
"target": 0.3,
"unit": "percentage"
}
},
"infrastructure": {
"container_orchestrator": "kubernetes",
"replicas": 4,
"cpu_limit": "1000m",
"memory_limit": "2Gi",
"storage": {
"type": "nfs",
"size": "50Gi"
},
"ingress": {
"type": "nginx",
"ssl_termination": true,
"rate_limiting": {
"requests_per_second": 100,
"burst": 200
}
}
},
"monitoring": {
"synthetic_checks": [
{
"name": "login_flow",
"url": "/auth/login",
"frequency": "1m",
"locations": [
"us-east",
"eu-west",
"ap-south"
]
},
{
"name": "checkout_flow",
"url": "/billing/checkout",
"frequency": "5m",
"locations": [
"us-east",
"eu-west"
]
}
],
"rum": {
"enabled": true,
"sampling_rate": 0.1
}
},
"compliance_requirements": [
"GDPR",
"CCPA"
],
"tags": [
"frontend",
"customer-facing",
"billing",
"high-traffic"
]
},
"target_role": "sre",
"generated_at": "2026-02-16T14:02:03.421248Z",
"version": "1.0"
},
"configuration": {
"time_ranges": [
"1h",
"6h",
"1d",
"7d"
],
"default_time_range": "6h",
"refresh_interval": "30s",
"timezone": "UTC",
"theme": "dark"
},
"layout": {
"grid_settings": {
"width": 24,
"height_unit": "px",
"cell_height": 30
},
"sections": [
{
"title": "Service Overview",
"collapsed": false,
"y_position": 0,
"panels": [
"service_status",
"slo_summary",
"error_budget"
]
},
{
"title": "Golden Signals",
"collapsed": false,
"y_position": 8,
"panels": [
"latency",
"traffic",
"errors",
"saturation"
]
},
{
"title": "Resource Utilization",
"collapsed": false,
"y_position": 16,
"panels": [
"cpu_usage",
"memory_usage",
"network_io",
"disk_io"
]
},
{
"title": "Dependencies & Downstream",
"collapsed": true,
"y_position": 24,
"panels": [
"dependency_status",
"downstream_latency",
"circuit_breakers"
]
}
]
},
"panels": [
{
"id": "service_status",
"title": "Service Status",
"type": "stat",
"grid_pos": {
"x": 0,
"y": 0,
"w": 6,
"h": 4
},
"targets": [
{
"expr": "up{service=\"customer-portal\"}",
"legendFormat": "Status"
}
],
"field_config": {
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Status"
},
"properties": [
{
"id": "color",
"value": {
"mode": "thresholds"
}
},
{
"id": "thresholds",
"value": {
"steps": [
{
"color": "red",
"value": 0
},
{
"color": "green",
"value": 1
}
]
}
},
{
"id": "mappings",
"value": [
{
"options": {
"0": {
"text": "DOWN"
}
},
"type": "value"
},
{
"options": {
"1": {
"text": "UP"
}
},
"type": "value"
}
]
}
]
}
]
},
"options": {
"orientation": "horizontal",
"textMode": "value_and_name"
}
},
{
"id": "slo_summary",
"title": "SLO Achievement (30d)",
"type": "stat",
"grid_pos": {
"x": 6,
"y": 0,
"w": 9,
"h": 4
},
"targets": [
{
"expr": "(1 - (increase(http_requests_total{service=\"customer-portal\",code=~\"5..\"}[30d]) / increase(http_requests_total{service=\"customer-portal\"}[30d]))) * 100",
"legendFormat": "Availability"
},
{
"expr": "histogram_quantile(0.95, increase(http_request_duration_seconds_bucket{service=\"customer-portal\"}[30d])) * 1000",
"legendFormat": "P95 Latency (ms)"
}
],
"field_config": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"steps": [
{
"color": "red",
"value": 0
},
{
"color": "yellow",
"value": 99.0
},
{
"color": "green",
"value": 99.9
}
]
}
}
},
"options": {
"orientation": "horizontal",
"textMode": "value_and_name"
}
},
{
"id": "error_budget",
"title": "Error Budget Remaining",
"type": "gauge",
"grid_pos": {
"x": 15,
"y": 0,
"w": 9,
"h": 4
},
"targets": [
{
"expr": "(1 - (increase(http_requests_total{service=\"customer-portal\",code=~\"5..\"}[30d]) / increase(http_requests_total{service=\"customer-portal\"}[30d])) - 0.999) / 0.001 * 100",
"legendFormat": "Error Budget %"
}
],
"field_config": {
"defaults": {
"color": {
"mode": "thresholds"
},
"min": 0,
"max": 100,
"thresholds": {
"steps": [
{
"color": "red",
"value": 0
},
{
"color": "yellow",
"value": 25
},
{
"color": "green",
"value": 50
}
]
},
"unit": "percent"
}
},
"options": {
"showThresholdLabels": true,
"showThresholdMarkers": true
}
},
{
"id": "latency",
"title": "Request Latency",
"type": "timeseries",
"grid_pos": {
"x": 0,
"y": 8,
"w": 12,
"h": 6
},
"targets": [
{
"expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket{service=\"customer-portal\"}[5m])) * 1000",
"legendFormat": "P50 Latency"
},
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{service=\"customer-portal\"}[5m])) * 1000",
"legendFormat": "P95 Latency"
},
{
"expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{service=\"customer-portal\"}[5m])) * 1000",
"legendFormat": "P99 Latency"
}
],
"field_config": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "ms",
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"lineWidth": 1,
"fillOpacity": 10
}
}
},
"options": {
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom"
}
}
},
{
"id": "traffic",
"title": "Request Rate",
"type": "timeseries",
"grid_pos": {
"x": 12,
"y": 8,
"w": 12,
"h": 6
},
"targets": [
{
"expr": "sum(rate(http_requests_total{service=\"customer-portal\"}[5m]))",
"legendFormat": "Total RPS"
},
{
"expr": "sum(rate(http_requests_total{service=\"customer-portal\",code=~\"2..\"}[5m]))",
"legendFormat": "2xx RPS"
},
{
"expr": "sum(rate(http_requests_total{service=\"customer-portal\",code=~\"4..\"}[5m]))",
"legendFormat": "4xx RPS"
},
{
"expr": "sum(rate(http_requests_total{service=\"customer-portal\",code=~\"5..\"}[5m]))",
"legendFormat": "5xx RPS"
}
],
"field_config": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "reqps",
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"lineWidth": 1,
"fillOpacity": 0
}
}
},
"options": {
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom"
}
}
},
{
"id": "errors",
"title": "Error Rate",
"type": "timeseries",
"grid_pos": {
"x": 0,
"y": 14,
"w": 12,
"h": 6
},
"targets": [
{
"expr": "sum(rate(http_requests_total{service=\"customer-portal\",code=~\"5..\"}[5m])) / sum(rate(http_requests_total{service=\"customer-portal\"}[5m])) * 100",
"legendFormat": "5xx Error Rate"
},
{
"expr": "sum(rate(http_requests_total{service=\"customer-portal\",code=~\"4..\"}[5m])) / sum(rate(http_requests_total{service=\"customer-portal\"}[5m])) * 100",
"legendFormat": "4xx Error Rate"
}
],
"field_config": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"lineWidth": 2,
"fillOpacity": 20
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "5xx Error Rate"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "red"
}
}
]
}
]
},
"options": {
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom"
}
}
},
{
"id": "saturation",
"title": "Saturation Metrics",
"type": "timeseries",
"grid_pos": {
"x": 12,
"y": 14,
"w": 12,
"h": 6
},
"targets": [
{
"expr": "rate(process_cpu_seconds_total{service=\"customer-portal\"}[5m]) * 100",
"legendFormat": "CPU Usage %"
},
{
"expr": "process_resident_memory_bytes{service=\"customer-portal\"} / process_virtual_memory_max_bytes{service=\"customer-portal\"} * 100",
"legendFormat": "Memory Usage %"
}
],
"field_config": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"max": 100,
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"lineWidth": 1,
"fillOpacity": 10
}
}
},
"options": {
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom"
}
}
},
{
"id": "cpu_usage",
"title": "CPU Usage",
"type": "gauge",
"grid_pos": {
"x": 0,
"y": 20,
"w": 6,
"h": 4
},
"targets": [
{
"expr": "rate(process_cpu_seconds_total{service=\"customer-portal\"}[5m]) * 100",
"legendFormat": "CPU %"
}
],
"field_config": {
"defaults": {
"color": {
"mode": "thresholds"
},
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
}
},
"options": {
"showThresholdLabels": true,
"showThresholdMarkers": true
}
},
{
"id": "memory_usage",
"title": "Memory Usage",
"type": "gauge",
"grid_pos": {
"x": 6,
"y": 20,
"w": 6,
"h": 4
},
"targets": [
{
"expr": "process_resident_memory_bytes{service=\"customer-portal\"} / 1024 / 1024",
"legendFormat": "Memory MB"
}
],
"field_config": {
"defaults": {
"color": {
"mode": "thresholds"
},
"unit": "decbytes",
"thresholds": {
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "yellow",
"value": 512000000
},
{
"color": "red",
"value": 1024000000
}
]
}
}
}
},
{
"id": "network_io",
"title": "Network I/O",
"type": "timeseries",
"grid_pos": {
"x": 12,
"y": 20,
"w": 6,
"h": 4
},
"targets": [
{
"expr": "rate(process_network_receive_bytes_total{service=\"customer-portal\"}[5m])",
"legendFormat": "RX Bytes/s"
},
{
"expr": "rate(process_network_transmit_bytes_total{service=\"customer-portal\"}[5m])",
"legendFormat": "TX Bytes/s"
}
],
"field_config": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "binBps"
}
}
},
{
"id": "disk_io",
"title": "Disk I/O",
"type": "timeseries",
"grid_pos": {
"x": 18,
"y": 20,
"w": 6,
"h": 4
},
"targets": [
{
"expr": "rate(process_disk_read_bytes_total{service=\"customer-portal\"}[5m])",
"legendFormat": "Read Bytes/s"
},
{
"expr": "rate(process_disk_write_bytes_total{service=\"customer-portal\"}[5m])",
"legendFormat": "Write Bytes/s"
}
],
"field_config": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "binBps"
}
}
}
],
"variables": [
{
"name": "environment",
"type": "query",
"query": "label_values(environment)",
"current": {
"text": "production",
"value": "production"
},
"includeAll": false,
"multi": false,
"refresh": "on_dashboard_load"
},
{
"name": "instance",
"type": "query",
"query": "label_values(up{service=\"customer-portal\"}, instance)",
"current": {
"text": "All",
"value": "$__all"
},
"includeAll": true,
"multi": true,
"refresh": "on_time_range_change"
},
{
"name": "handler",
"type": "query",
"query": "label_values(http_requests_total{service=\"customer-portal\"}, handler)",
"current": {
"text": "All",
"value": "$__all"
},
"includeAll": true,
"multi": true,
"refresh": "on_time_range_change"
}
],
"alerts_integration": {
"alert_annotations": true,
"alert_rules_query": "ALERTS{service=\"customer-portal\"}",
"alert_panels": [
{
"title": "Active Alerts",
"type": "table",
"query": "ALERTS{service=\"customer-portal\",alertstate=\"firing\"}",
"columns": [
"alertname",
"severity",
"instance",
"description"
]
}
]
},
"drill_down_paths": {
"service_overview": {
"from": "service_status",
"to": "detailed_health_dashboard",
"url": "/d/service-health/customer-portal-health",
"params": [
"var-service",
"var-environment"
]
},
"error_investigation": {
"from": "errors",
"to": "error_details_dashboard",
"url": "/d/errors/customer-portal-errors",
"params": [
"var-service",
"var-time_range"
]
},
"latency_analysis": {
"from": "latency",
"to": "trace_analysis_dashboard",
"url": "/d/traces/customer-portal-traces",
"params": [
"var-service",
"var-handler"
]
},
"capacity_planning": {
"from": "saturation",
"to": "capacity_dashboard",
"url": "/d/capacity/customer-portal-capacity",
"params": [
"var-service",
"var-time_range"
]
}
}
}
FILE:expected_outputs/sample_slo_framework.json
{
"metadata": {
"service": {
"name": "payment-service",
"type": "api",
"criticality": "critical",
"user_facing": true,
"description": "Handles payment processing and transaction management",
"team": "payments",
"environment": "production",
"dependencies": [
{
"name": "user-service",
"type": "api",
"criticality": "high"
},
{
"name": "payment-gateway",
"type": "external",
"criticality": "critical"
},
{
"name": "fraud-detection",
"type": "ml",
"criticality": "high"
}
],
"endpoints": [
{
"path": "/api/v1/payments",
"method": "POST",
"sla_latency_ms": 500,
"expected_tps": 100
},
{
"path": "/api/v1/payments/{id}",
"method": "GET",
"sla_latency_ms": 200,
"expected_tps": 500
},
{
"path": "/api/v1/payments/{id}/refund",
"method": "POST",
"sla_latency_ms": 1000,
"expected_tps": 10
}
],
"business_metrics": {
"revenue_per_hour": {
"metric": "sum(payment_amount * rate(payments_successful_total[1h]))",
"target": 50000,
"unit": "USD"
},
"conversion_rate": {
"metric": "sum(rate(payments_successful_total[5m])) / sum(rate(payment_attempts_total[5m]))",
"target": 0.95,
"unit": "percentage"
}
},
"infrastructure": {
"container_orchestrator": "kubernetes",
"replicas": 6,
"cpu_limit": "2000m",
"memory_limit": "4Gi",
"database": {
"type": "postgresql",
"connection_pool_size": 20
},
"cache": {
"type": "redis",
"cluster_size": 3
}
},
"compliance_requirements": [
"PCI-DSS",
"SOX",
"GDPR"
],
"tags": [
"payment",
"transaction",
"critical-path",
"revenue-generating"
]
},
"generated_at": "2026-02-16T14:01:57.572080Z",
"framework_version": "1.0"
},
"slis": [
{
"name": "Availability",
"description": "Percentage of successful requests",
"type": "ratio",
"good_events": "sum(rate(http_requests_total{service=\"payment-service\",code!~\"5..\"}))",
"total_events": "sum(rate(http_requests_total{service=\"payment-service\"}))",
"unit": "percentage"
},
{
"name": "Request Latency P95",
"description": "95th percentile of request latency",
"type": "threshold",
"query": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{service=\"payment-service\"}[5m]))",
"unit": "seconds"
},
{
"name": "Error Rate",
"description": "Rate of 5xx errors",
"type": "ratio",
"good_events": "sum(rate(http_requests_total{service=\"payment-service\",code!~\"5..\"}))",
"total_events": "sum(rate(http_requests_total{service=\"payment-service\"}))",
"unit": "percentage"
},
{
"name": "Request Throughput",
"description": "Requests per second",
"type": "gauge",
"query": "sum(rate(http_requests_total{service=\"payment-service\"}[5m]))",
"unit": "requests/sec"
},
{
"name": "User Journey Success Rate",
"description": "Percentage of successful complete user journeys",
"type": "ratio",
"good_events": "sum(rate(user_journey_total{service=\"payment-service\",status=\"success\"}[5m]))",
"total_events": "sum(rate(user_journey_total{service=\"payment-service\"}[5m]))",
"unit": "percentage"
},
{
"name": "Feature Availability",
"description": "Percentage of time key features are available",
"type": "ratio",
"good_events": "sum(rate(feature_checks_total{service=\"payment-service\",status=\"available\"}[5m]))",
"total_events": "sum(rate(feature_checks_total{service=\"payment-service\"}[5m]))",
"unit": "percentage"
}
],
"slos": [
{
"name": "Availability SLO",
"description": "Service level objective for percentage of successful requests",
"sli_name": "Availability",
"target_value": 0.9999,
"target_display": "99.99%",
"operator": ">=",
"time_windows": [
"1h",
"1d",
"7d",
"30d"
],
"measurement_window": "30d",
"service": "payment-service",
"criticality": "critical"
},
{
"name": "Request Latency P95 SLO",
"description": "Service level objective for 95th percentile of request latency",
"sli_name": "Request Latency P95",
"target_value": 100,
"target_display": "0.1s",
"operator": "<=",
"time_windows": [
"1h",
"1d",
"7d",
"30d"
],
"measurement_window": "30d",
"service": "payment-service",
"criticality": "critical"
},
{
"name": "Error Rate SLO",
"description": "Service level objective for rate of 5xx errors",
"sli_name": "Error Rate",
"target_value": 0.001,
"target_display": "0.1%",
"operator": "<=",
"time_windows": [
"1h",
"1d",
"7d",
"30d"
],
"measurement_window": "30d",
"service": "payment-service",
"criticality": "critical"
},
{
"name": "User Journey Success Rate SLO",
"description": "Service level objective for percentage of successful complete user journeys",
"sli_name": "User Journey Success Rate",
"target_value": 0.9999,
"target_display": "99.99%",
"operator": ">=",
"time_windows": [
"1h",
"1d",
"7d",
"30d"
],
"measurement_window": "30d",
"service": "payment-service",
"criticality": "critical"
},
{
"name": "Feature Availability SLO",
"description": "Service level objective for percentage of time key features are available",
"sli_name": "Feature Availability",
"target_value": 0.9999,
"target_display": "99.99%",
"operator": ">=",
"time_windows": [
"1h",
"1d",
"7d",
"30d"
],
"measurement_window": "30d",
"service": "payment-service",
"criticality": "critical"
}
],
"error_budgets": [
{
"slo_name": "Availability SLO",
"error_budget_rate": 9.999999999998899e-05,
"error_budget_percentage": "0.010%",
"budgets_by_window": {
"1h": "0.4 seconds",
"1d": "8.6 seconds",
"7d": "1.0 minutes",
"30d": "4.3 minutes"
},
"burn_rate_alerts": [
{
"name": "Availability Burn Rate 2% Alert",
"description": "Alert when Availability is consuming error budget at 14.4x rate",
"severity": "critical",
"short_window": "5m",
"long_window": "1h",
"burn_rate_threshold": 14.4,
"budget_consumed": "2%",
"condition": "((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_short > 14.4) and ((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_long > 14.4)",
"annotations": {
"summary": "High burn rate detected for Availability",
"description": "Error budget consumption rate is 14.4x normal, will exhaust 2% of monthly budget"
}
},
{
"name": "Availability Burn Rate 5% Alert",
"description": "Alert when Availability is consuming error budget at 6x rate",
"severity": "warning",
"short_window": "30m",
"long_window": "6h",
"burn_rate_threshold": 6,
"budget_consumed": "5%",
"condition": "((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_short > 6) and ((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_long > 6)",
"annotations": {
"summary": "High burn rate detected for Availability",
"description": "Error budget consumption rate is 6x normal, will exhaust 5% of monthly budget"
}
},
{
"name": "Availability Burn Rate 10% Alert",
"description": "Alert when Availability is consuming error budget at 3x rate",
"severity": "info",
"short_window": "2h",
"long_window": "1d",
"burn_rate_threshold": 3,
"budget_consumed": "10%",
"condition": "((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_short > 3) and ((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_long > 3)",
"annotations": {
"summary": "High burn rate detected for Availability",
"description": "Error budget consumption rate is 3x normal, will exhaust 10% of monthly budget"
}
},
{
"name": "Availability Burn Rate 10% Alert",
"description": "Alert when Availability is consuming error budget at 1x rate",
"severity": "info",
"short_window": "6h",
"long_window": "3d",
"burn_rate_threshold": 1,
"budget_consumed": "10%",
"condition": "((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_short > 1) and ((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_long > 1)",
"annotations": {
"summary": "High burn rate detected for Availability",
"description": "Error budget consumption rate is 1x normal, will exhaust 10% of monthly budget"
}
}
]
},
{
"slo_name": "User Journey Success Rate SLO",
"error_budget_rate": 9.999999999998899e-05,
"error_budget_percentage": "0.010%",
"budgets_by_window": {
"1h": "0.4 seconds",
"1d": "8.6 seconds",
"7d": "1.0 minutes",
"30d": "4.3 minutes"
},
"burn_rate_alerts": [
{
"name": "User Journey Success Rate Burn Rate 2% Alert",
"description": "Alert when User Journey Success Rate is consuming error budget at 14.4x rate",
"severity": "critical",
"short_window": "5m",
"long_window": "1h",
"burn_rate_threshold": 14.4,
"budget_consumed": "2%",
"condition": "((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_short > 14.4) and ((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_long > 14.4)",
"annotations": {
"summary": "High burn rate detected for User Journey Success Rate",
"description": "Error budget consumption rate is 14.4x normal, will exhaust 2% of monthly budget"
}
},
{
"name": "User Journey Success Rate Burn Rate 5% Alert",
"description": "Alert when User Journey Success Rate is consuming error budget at 6x rate",
"severity": "warning",
"short_window": "30m",
"long_window": "6h",
"burn_rate_threshold": 6,
"budget_consumed": "5%",
"condition": "((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_short > 6) and ((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_long > 6)",
"annotations": {
"summary": "High burn rate detected for User Journey Success Rate",
"description": "Error budget consumption rate is 6x normal, will exhaust 5% of monthly budget"
}
},
{
"name": "User Journey Success Rate Burn Rate 10% Alert",
"description": "Alert when User Journey Success Rate is consuming error budget at 3x rate",
"severity": "info",
"short_window": "2h",
"long_window": "1d",
"burn_rate_threshold": 3,
"budget_consumed": "10%",
"condition": "((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_short > 3) and ((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_long > 3)",
"annotations": {
"summary": "High burn rate detected for User Journey Success Rate",
"description": "Error budget consumption rate is 3x normal, will exhaust 10% of monthly budget"
}
},
{
"name": "User Journey Success Rate Burn Rate 10% Alert",
"description": "Alert when User Journey Success Rate is consuming error budget at 1x rate",
"severity": "info",
"short_window": "6h",
"long_window": "3d",
"burn_rate_threshold": 1,
"budget_consumed": "10%",
"condition": "((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_short > 1) and ((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_long > 1)",
"annotations": {
"summary": "High burn rate detected for User Journey Success Rate",
"description": "Error budget consumption rate is 1x normal, will exhaust 10% of monthly budget"
}
}
]
},
{
"slo_name": "Feature Availability SLO",
"error_budget_rate": 9.999999999998899e-05,
"error_budget_percentage": "0.010%",
"budgets_by_window": {
"1h": "0.4 seconds",
"1d": "8.6 seconds",
"7d": "1.0 minutes",
"30d": "4.3 minutes"
},
"burn_rate_alerts": [
{
"name": "Feature Availability Burn Rate 2% Alert",
"description": "Alert when Feature Availability is consuming error budget at 14.4x rate",
"severity": "critical",
"short_window": "5m",
"long_window": "1h",
"burn_rate_threshold": 14.4,
"budget_consumed": "2%",
"condition": "((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_short > 14.4) and ((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_long > 14.4)",
"annotations": {
"summary": "High burn rate detected for Feature Availability",
"description": "Error budget consumption rate is 14.4x normal, will exhaust 2% of monthly budget"
}
},
{
"name": "Feature Availability Burn Rate 5% Alert",
"description": "Alert when Feature Availability is consuming error budget at 6x rate",
"severity": "warning",
"short_window": "30m",
"long_window": "6h",
"burn_rate_threshold": 6,
"budget_consumed": "5%",
"condition": "((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_short > 6) and ((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_long > 6)",
"annotations": {
"summary": "High burn rate detected for Feature Availability",
"description": "Error budget consumption rate is 6x normal, will exhaust 5% of monthly budget"
}
},
{
"name": "Feature Availability Burn Rate 10% Alert",
"description": "Alert when Feature Availability is consuming error budget at 3x rate",
"severity": "info",
"short_window": "2h",
"long_window": "1d",
"burn_rate_threshold": 3,
"budget_consumed": "10%",
"condition": "((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_short > 3) and ((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_long > 3)",
"annotations": {
"summary": "High burn rate detected for Feature Availability",
"description": "Error budget consumption rate is 3x normal, will exhaust 10% of monthly budget"
}
},
{
"name": "Feature Availability Burn Rate 10% Alert",
"description": "Alert when Feature Availability is consuming error budget at 1x rate",
"severity": "info",
"short_window": "6h",
"long_window": "3d",
"burn_rate_threshold": 1,
"budget_consumed": "10%",
"condition": "((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_short > 1) and ((1 - (sum(rate(http_requests_total{service='payment-service',code!~'5..'})) / sum(rate(http_requests_total{service='payment-service'}))))_long > 1)",
"annotations": {
"summary": "High burn rate detected for Feature Availability",
"description": "Error budget consumption rate is 1x normal, will exhaust 10% of monthly budget"
}
}
]
}
],
"sla_recommendations": {
"applicable": true,
"service": "payment-service",
"commitments": [
{
"metric": "Availability",
"target": 0.9989,
"target_display": "99.89%",
"measurement_window": "monthly",
"measurement_method": "Uptime monitoring with 1-minute granularity"
},
{
"metric": "Feature Availability",
"target": 0.9989,
"target_display": "99.89%",
"measurement_window": "monthly",
"measurement_method": "Uptime monitoring with 1-minute granularity"
}
],
"penalties": [
{
"breach_threshold": "< 99.99%",
"credit_percentage": 10
},
{
"breach_threshold": "< 99.9%",
"credit_percentage": 25
},
{
"breach_threshold": "< 99%",
"credit_percentage": 50
}
],
"measurement_methodology": "External synthetic monitoring from multiple geographic locations",
"exclusions": [
"Planned maintenance windows (with 72h advance notice)",
"Customer-side network or infrastructure issues",
"Force majeure events",
"Third-party service dependencies beyond our control"
]
},
"monitoring_recommendations": {
"metrics": {
"collection": "Prometheus with service discovery",
"retention": "90 days for raw metrics, 1 year for aggregated",
"alerting": "Prometheus Alertmanager with multi-window burn rate alerts"
},
"logging": {
"format": "Structured JSON logs with correlation IDs",
"aggregation": "ELK stack or equivalent with proper indexing",
"retention": "30 days for debug logs, 90 days for error logs"
},
"tracing": {
"sampling": "Adaptive sampling with 1% base rate",
"storage": "Jaeger or Zipkin with 7-day retention",
"integration": "OpenTelemetry instrumentation"
}
},
"implementation_guide": {
"prerequisites": [
"Service instrumented with metrics collection (Prometheus format)",
"Structured logging with correlation IDs",
"Monitoring infrastructure (Prometheus, Grafana, Alertmanager)",
"Incident response processes and escalation policies"
],
"implementation_steps": [
{
"step": 1,
"title": "Instrument Service",
"description": "Add metrics collection for all defined SLIs",
"estimated_effort": "1-2 days"
},
{
"step": 2,
"title": "Configure Recording Rules",
"description": "Set up Prometheus recording rules for SLI calculations",
"estimated_effort": "4-8 hours"
},
{
"step": 3,
"title": "Implement Burn Rate Alerts",
"description": "Configure multi-window burn rate alerting rules",
"estimated_effort": "1 day"
},
{
"step": 4,
"title": "Create SLO Dashboard",
"description": "Build Grafana dashboard for SLO tracking and error budget monitoring",
"estimated_effort": "4-6 hours"
},
{
"step": 5,
"title": "Test and Validate",
"description": "Test alerting and validate SLI measurements against expectations",
"estimated_effort": "1-2 days"
},
{
"step": 6,
"title": "Documentation and Training",
"description": "Document runbooks and train team on SLO monitoring",
"estimated_effort": "1 day"
}
],
"validation_checklist": [
"All SLIs produce expected metric values",
"Burn rate alerts fire correctly during simulated outages",
"Error budget calculations match manual verification",
"Dashboard displays accurate SLO achievement rates",
"Alert routing reaches correct escalation paths",
"Runbooks are complete and tested"
]
}
}
FILE:references/alert_design_patterns.md
# Alert Design Patterns: A Guide to Effective Alerting
## Introduction
Well-designed alerts are the difference between a reliable system and 3 AM pages about non-issues. This guide provides patterns and anti-patterns for creating alerts that provide value without causing fatigue.
## Fundamental Principles
### The Golden Rules of Alerting
1. **Every alert should be actionable** - If you can't do something about it, don't alert
2. **Every alert should require human intelligence** - If a script can handle it, automate the response
3. **Every alert should be novel** - Don't alert on known, ongoing issues
4. **Every alert should represent a user-visible impact** - Internal metrics matter only if users are affected
### Alert Classification
#### Critical Alerts
- Service is completely down
- Data loss is occurring
- Security breach detected
- SLO burn rate indicates imminent SLO violation
#### Warning Alerts
- Service degradation affecting some users
- Approaching resource limits
- Dependent service issues
- Elevated error rates within SLO
#### Info Alerts
- Deployment notifications
- Capacity planning triggers
- Configuration changes
- Maintenance windows
## Alert Design Patterns
### Pattern 1: Symptoms, Not Causes
**Good**: Alert on user-visible symptoms
```yaml
- alert: HighLatency
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 0.5
for: 5m
annotations:
summary: "API latency is high"
description: "95th percentile latency is {{ $value }}s, above 500ms threshold"
```
**Bad**: Alert on internal metrics that may not affect users
```yaml
- alert: HighCPU
expr: cpu_usage > 80
# This might not affect users at all!
```
### Pattern 2: Multi-Window Alerting
Reduce false positives by requiring sustained problems:
```yaml
- alert: ServiceDown
expr: (
avg_over_time(up[2m]) == 0 # Short window: immediate detection
and
avg_over_time(up[10m]) < 0.8 # Long window: avoid flapping
)
for: 1m
```
### Pattern 3: Burn Rate Alerting
Alert based on error budget consumption rate:
```yaml
# Fast burn: 2% of monthly budget in 1 hour
- alert: ErrorBudgetFastBurn
expr: (
error_rate_5m > (14.4 * error_budget_slo)
and
error_rate_1h > (14.4 * error_budget_slo)
)
for: 2m
labels:
severity: critical
# Slow burn: 10% of monthly budget in 3 days
- alert: ErrorBudgetSlowBurn
expr: (
error_rate_6h > (1.0 * error_budget_slo)
and
error_rate_3d > (1.0 * error_budget_slo)
)
for: 15m
labels:
severity: warning
```
### Pattern 4: Hysteresis
Use different thresholds for firing and resolving to prevent flapping:
```yaml
- alert: HighErrorRate
expr: error_rate > 0.05 # Fire at 5%
for: 5m
# Resolution happens automatically when error_rate < 0.03 (3%)
# This prevents flapping around the 5% threshold
```
### Pattern 5: Composite Alerts
Alert when multiple conditions indicate a problem:
```yaml
- alert: ServiceDegraded
expr: (
(latency_p95 > latency_threshold)
or
(error_rate > error_threshold)
or
(availability < availability_threshold)
) and (
request_rate > min_request_rate # Only alert if we have traffic
)
```
### Pattern 6: Contextual Alerting
Include relevant context in alerts:
```yaml
- alert: DatabaseConnections
expr: db_connections_active / db_connections_max > 0.8
for: 5m
annotations:
summary: "Database connection pool nearly exhausted"
description: "{{ $labels.database }} has {{ $value | humanizePercentage }} connection utilization"
runbook_url: "https://runbooks.company.com/database-connections"
impact: "New requests may be rejected, causing 500 errors"
suggested_action: "Check for connection leaks or increase pool size"
```
## Alert Routing and Escalation
### Routing by Impact and Urgency
#### Critical Path Services
```yaml
route:
group_by: ['service']
routes:
- match:
service: 'payment-api'
severity: 'critical'
receiver: 'payment-team-pager'
continue: true
- match:
service: 'payment-api'
severity: 'warning'
receiver: 'payment-team-slack'
```
#### Time-Based Routing
```yaml
route:
routes:
- match:
severity: 'critical'
receiver: 'oncall-pager'
- match:
severity: 'warning'
time: 'business_hours' # 9 AM - 5 PM
receiver: 'team-slack'
- match:
severity: 'warning'
time: 'after_hours'
receiver: 'team-email' # Lower urgency outside business hours
```
### Escalation Patterns
#### Linear Escalation
```yaml
receivers:
- name: 'primary-oncall'
pagerduty_configs:
- escalation_policy: 'P1-Escalation'
# 0 min: Primary on-call
# 5 min: Secondary on-call
# 15 min: Engineering manager
# 30 min: Director of engineering
```
#### Severity-Based Escalation
```yaml
# Critical: Immediate escalation
- match:
severity: 'critical'
receiver: 'critical-escalation'
# Warning: Team-first escalation
- match:
severity: 'warning'
receiver: 'team-escalation'
```
## Alert Fatigue Prevention
### Grouping and Suppression
#### Time-Based Grouping
```yaml
route:
group_wait: 30s # Wait 30s to group similar alerts
group_interval: 2m # Send grouped alerts every 2 minutes
repeat_interval: 1h # Re-send unresolved alerts every hour
```
#### Dependent Service Suppression
```yaml
- alert: ServiceDown
expr: up == 0
- alert: HighLatency
expr: latency_p95 > 1
# This alert is suppressed when ServiceDown is firing
inhibit_rules:
- source_match:
alertname: 'ServiceDown'
target_match:
alertname: 'HighLatency'
equal: ['service']
```
### Alert Throttling
```yaml
# Limit to 1 alert per 10 minutes for noisy conditions
- alert: HighMemoryUsage
expr: memory_usage_percent > 85
for: 10m # Longer 'for' duration reduces noise
annotations:
summary: "Memory usage has been high for 10+ minutes"
```
### Smart Defaults
```yaml
# Use business logic to set intelligent thresholds
- alert: LowTraffic
expr: request_rate < (
avg_over_time(request_rate[7d]) * 0.1 # 10% of weekly average
)
# Only alert during business hours when low traffic is unusual
for: 30m
```
## Runbook Integration
### Runbook Structure Template
```markdown
# Alert: {{ $labels.alertname }}
## Immediate Actions
1. Check service status dashboard
2. Verify if users are affected
3. Look at recent deployments/changes
## Investigation Steps
1. Check logs for errors in the last 30 minutes
2. Verify dependent services are healthy
3. Check resource utilization (CPU, memory, disk)
4. Review recent alerts for patterns
## Resolution Actions
- If deployment-related: Consider rollback
- If resource-related: Scale up or optimize queries
- If dependency-related: Engage appropriate team
## Escalation
- Primary: @team-oncall
- Secondary: @engineering-manager
- Emergency: @site-reliability-team
```
### Runbook Integration in Alerts
```yaml
annotations:
runbook_url: "https://runbooks.company.com/alerts/{{ $labels.alertname }}"
quick_debug: |
1. curl -s https://{{ $labels.instance }}/health
2. kubectl logs {{ $labels.pod }} --tail=50
3. Check dashboard: https://grafana.company.com/d/service-{{ $labels.service }}
```
## Testing and Validation
### Alert Testing Strategies
#### Chaos Engineering Integration
```python
# Test that alerts fire during controlled failures
def test_alert_during_cpu_spike():
with chaos.cpu_spike(target='payment-api', duration='2m'):
assert wait_for_alert('HighCPU', timeout=180)
def test_alert_during_network_partition():
with chaos.network_partition(target='database'):
assert wait_for_alert('DatabaseUnreachable', timeout=60)
```
#### Historical Alert Analysis
```prometheus
# Query to find alerts that fired without incidents
count by (alertname) (
ALERTS{alertstate="firing"}[30d]
) unless on (alertname) (
count by (alertname) (
incident_created{source="alert"}[30d]
)
)
```
### Alert Quality Metrics
#### Alert Precision
```
Precision = True Positives / (True Positives + False Positives)
```
Track alerts that resulted in actual incidents vs false alarms.
#### Time to Resolution
```prometheus
# Average time from alert firing to resolution
avg_over_time(
(alert_resolved_timestamp - alert_fired_timestamp)[30d]
) by (alertname)
```
#### Alert Fatigue Indicators
```prometheus
# Alerts per day by team
sum by (team) (
increase(alerts_fired_total[1d])
)
# Percentage of alerts acknowledged within 15 minutes
sum(alerts_acked_within_15m) / sum(alerts_fired) * 100
```
## Advanced Patterns
### Machine Learning-Enhanced Alerting
#### Anomaly Detection
```yaml
- alert: AnomalousTraffic
expr: |
abs(request_rate - predict_linear(request_rate[1h], 300)) /
stddev_over_time(request_rate[1h]) > 3
for: 10m
annotations:
summary: "Traffic pattern is anomalous"
description: "Current traffic deviates from predicted pattern by >3 standard deviations"
```
#### Dynamic Thresholds
```yaml
- alert: DynamicHighLatency
expr: |
latency_p95 > (
quantile_over_time(0.95, latency_p95[7d]) + # Historical 95th percentile
2 * stddev_over_time(latency_p95[7d]) # Plus 2 standard deviations
)
```
### Business Hours Awareness
```yaml
# Different thresholds for business vs off hours
- alert: HighLatencyBusinessHours
expr: latency_p95 > 0.2 # Stricter during business hours
for: 2m
# Active 9 AM - 5 PM weekdays
- alert: HighLatencyOffHours
expr: latency_p95 > 0.5 # More lenient after hours
for: 5m
# Active nights and weekends
```
### Progressive Alerting
```yaml
# Escalating alert severity based on duration
- alert: ServiceLatencyElevated
expr: latency_p95 > 0.5
for: 5m
labels:
severity: info
- alert: ServiceLatencyHigh
expr: latency_p95 > 0.5
for: 15m # Same condition, longer duration
labels:
severity: warning
- alert: ServiceLatencyCritical
expr: latency_p95 > 0.5
for: 30m # Same condition, even longer duration
labels:
severity: critical
```
## Anti-Patterns to Avoid
### Anti-Pattern 1: Alerting on Everything
**Problem**: Too many alerts create noise and fatigue
**Solution**: Be selective; only alert on user-impacting issues
### Anti-Pattern 2: Vague Alert Messages
**Problem**: "Service X is down" - which instance? what's the impact?
**Solution**: Include specific details and context
### Anti-Pattern 3: Alerts Without Runbooks
**Problem**: Alerts that don't explain what to do
**Solution**: Every alert must have an associated runbook
### Anti-Pattern 4: Static Thresholds
**Problem**: 80% CPU might be normal during peak hours
**Solution**: Use contextual, adaptive thresholds
### Anti-Pattern 5: Ignoring Alert Quality
**Problem**: Accepting high false positive rates
**Solution**: Regularly review and tune alert precision
## Implementation Checklist
### Pre-Implementation
- [ ] Define alert severity levels and escalation policies
- [ ] Create runbook templates
- [ ] Set up alert routing configuration
- [ ] Define SLOs that alerts will protect
### Alert Development
- [ ] Each alert has clear success criteria
- [ ] Alert conditions tested against historical data
- [ ] Runbook created and accessible
- [ ] Severity and routing configured
- [ ] Context and suggested actions included
### Post-Implementation
- [ ] Monitor alert precision and recall
- [ ] Regular review of alert fatigue metrics
- [ ] Quarterly alert effectiveness review
- [ ] Team training on alert response procedures
### Quality Assurance
- [ ] Test alerts fire during controlled failures
- [ ] Verify alerts resolve when conditions improve
- [ ] Confirm runbooks are accurate and helpful
- [ ] Validate escalation paths work correctly
Remember: Great alerts are invisible when things work and invaluable when things break. Focus on quality over quantity, and always optimize for the human who will respond to the alert at 3 AM.
FILE:references/dashboard_best_practices.md
# Dashboard Best Practices: Design for Insight and Action
## Introduction
A well-designed dashboard is like a good story - it guides you through the data with purpose and clarity. This guide provides practical patterns for creating dashboards that inform decisions and enable quick troubleshooting.
## Design Principles
### The Hierarchy of Information
#### Primary Information (Top Third)
- Service health status
- SLO achievement
- Critical alerts
- Business KPIs
#### Secondary Information (Middle Third)
- Golden signals (latency, traffic, errors, saturation)
- Resource utilization
- Throughput and performance metrics
#### Tertiary Information (Bottom Third)
- Detailed breakdowns
- Historical trends
- Dependency status
- Debug information
### Visual Design Principles
#### Rule of 7±2
- Maximum 7±2 panels per screen
- Group related information together
- Use sections to organize complexity
#### Color Psychology
- **Red**: Critical issues, danger, immediate attention needed
- **Yellow/Orange**: Warnings, caution, degraded state
- **Green**: Healthy, normal operation, success
- **Blue**: Information, neutral metrics, capacity
- **Gray**: Disabled, unknown, or baseline states
#### Chart Selection Guide
- **Line charts**: Time series, trends, comparisons over time
- **Bar charts**: Categorical comparisons, top N lists
- **Gauges**: Single value with defined good/bad ranges
- **Stat panels**: Key metrics, percentages, counts
- **Heatmaps**: Distribution data, correlation analysis
- **Tables**: Detailed breakdowns, multi-dimensional data
## Dashboard Archetypes
### The Overview Dashboard
**Purpose**: High-level health check and business metrics
**Audience**: Executives, managers, cross-team stakeholders
**Update Frequency**: 5-15 minutes
```yaml
sections:
- title: "Business Health"
panels:
- service_availability_summary
- revenue_per_hour
- active_users
- conversion_rate
- title: "System Health"
panels:
- critical_alerts_count
- slo_achievement_summary
- error_budget_remaining
- deployment_status
```
### The SRE Operational Dashboard
**Purpose**: Real-time monitoring and incident response
**Audience**: SRE, on-call engineers
**Update Frequency**: 15-30 seconds
```yaml
sections:
- title: "Service Status"
panels:
- service_up_status
- active_incidents
- recent_deployments
- title: "Golden Signals"
panels:
- latency_percentiles
- request_rate
- error_rate
- resource_saturation
- title: "Infrastructure"
panels:
- cpu_memory_utilization
- network_io
- disk_space
```
### The Developer Debug Dashboard
**Purpose**: Deep-dive troubleshooting and performance analysis
**Audience**: Development teams
**Update Frequency**: 30 seconds - 2 minutes
```yaml
sections:
- title: "Application Performance"
panels:
- endpoint_latency_breakdown
- database_query_performance
- cache_hit_rates
- queue_depths
- title: "Errors and Logs"
panels:
- error_rate_by_endpoint
- log_volume_by_level
- exception_types
- slow_queries
```
## Layout Patterns
### The F-Pattern Layout
Based on eye-tracking studies, users scan in an F-pattern:
```
[Critical Status] [SLO Summary ] [Error Budget ]
[Latency ] [Traffic ] [Errors ]
[Saturation ] [Resource Use ] [Detailed View]
[Historical ] [Dependencies ] [Debug Info ]
```
### The Z-Pattern Layout
For executive dashboards, follow the Z-pattern:
```
[Business KPIs ] → [System Status]
↓ ↓
[Trend Analysis ] ← [Key Metrics ]
```
### Responsive Design
#### Desktop (1920x1080)
- 24-column grid
- Panels can be 6, 8, 12, or 24 units wide
- 4-6 rows visible without scrolling
#### Laptop (1366x768)
- Stack wider panels vertically
- Reduce panel heights
- Prioritize most critical information
#### Mobile (768px width)
- Single column layout
- Simplified panels
- Touch-friendly controls
## Effective Panel Design
### Stat Panels
```yaml
# Good: Clear value with context
- title: "API Availability"
type: stat
targets:
- expr: avg(up{service="api"}) * 100
field_config:
unit: percent
thresholds:
steps:
- color: red
value: 0
- color: yellow
value: 99
- color: green
value: 99.9
options:
color_mode: background
text_mode: value_and_name
```
### Time Series Panels
```yaml
# Good: Multiple related metrics with clear legend
- title: "Request Latency"
type: timeseries
targets:
- expr: histogram_quantile(0.50, rate(http_duration_bucket[5m]))
legend: "P50"
- expr: histogram_quantile(0.95, rate(http_duration_bucket[5m]))
legend: "P95"
- expr: histogram_quantile(0.99, rate(http_duration_bucket[5m]))
legend: "P99"
field_config:
unit: ms
custom:
draw_style: line
fill_opacity: 10
options:
legend:
display_mode: table
placement: bottom
values: [min, max, mean, last]
```
### Table Panels
```yaml
# Good: Top N with relevant columns
- title: "Slowest Endpoints"
type: table
targets:
- expr: topk(10, histogram_quantile(0.95, sum by (handler)(rate(http_duration_bucket[5m]))))
format: table
instant: true
transformations:
- id: organize
options:
exclude_by_name:
Time: true
rename_by_name:
Value: "P95 Latency (ms)"
handler: "Endpoint"
```
## Color and Visualization Best Practices
### Threshold Configuration
```yaml
# Traffic light system with meaningful boundaries
thresholds:
steps:
- color: green # Good performance
value: null # Default
- color: yellow # Degraded performance
value: 95 # 95th percentile of historical normal
- color: orange # Poor performance
value: 99 # 99th percentile of historical normal
- color: red # Critical performance
value: 99.9 # Worst case scenario
```
### Color Blind Friendly Palettes
```yaml
# Use patterns and shapes in addition to color
field_config:
overrides:
- matcher:
id: byName
options: "Critical"
properties:
- id: color
value:
mode: fixed
fixed_color: "#d73027" # Red-orange for protanopia
- id: custom.draw_style
value: "points" # Different shape
```
### Consistent Color Semantics
- **Success/Health**: Green (#28a745)
- **Warning/Degraded**: Yellow (#ffc107)
- **Error/Critical**: Red (#dc3545)
- **Information**: Blue (#007bff)
- **Neutral**: Gray (#6c757d)
## Time Range Strategy
### Default Time Ranges by Dashboard Type
#### Real-time Operational
- **Default**: Last 15 minutes
- **Quick options**: 5m, 15m, 1h, 4h
- **Auto-refresh**: 15-30 seconds
#### Troubleshooting
- **Default**: Last 1 hour
- **Quick options**: 15m, 1h, 4h, 12h, 1d
- **Auto-refresh**: 1 minute
#### Business Review
- **Default**: Last 24 hours
- **Quick options**: 1d, 7d, 30d, 90d
- **Auto-refresh**: 5 minutes
#### Capacity Planning
- **Default**: Last 7 days
- **Quick options**: 7d, 30d, 90d, 1y
- **Auto-refresh**: 15 minutes
### Time Range Annotations
```yaml
# Add context for time-based events
annotations:
- name: "Deployments"
datasource: "Prometheus"
expr: "deployment_timestamp"
title_format: "Deploy {{ version }}"
text_format: "Deployed version {{ version }} to {{ environment }}"
- name: "Incidents"
datasource: "Incident API"
query: "incidents.json?service={{ service }}"
color: "red"
```
## Interactive Features
### Template Variables
```yaml
# Service selector
- name: service
type: query
query: label_values(up, service)
current:
text: All
value: $__all
include_all: true
multi: true
# Environment selector
- name: environment
type: query
query: label_values(up{service="$service"}, environment)
current:
text: production
value: production
```
### Drill-Down Links
```yaml
# Panel-level drill-downs
- title: "Error Rate"
type: timeseries
# ... other config ...
options:
data_links:
- title: "View Error Logs"
url: "/d/logs-dashboard?var-service=__field.labels.service&from=__from&to=__to"
- title: "Error Traces"
url: "/d/traces-dashboard?var-service=__field.labels.service"
```
### Dynamic Panel Titles
```yaml
- title: "service - Request Rate" # Uses template variable
type: timeseries
# Title updates automatically when service variable changes
```
## Performance Optimization
### Query Optimization
#### Use Recording Rules
```yaml
# Instead of complex queries in dashboards
groups:
- name: http_requests
rules:
- record: http_request_rate_5m
expr: sum(rate(http_requests_total[5m])) by (service, method, handler)
- record: http_request_latency_p95_5m
expr: histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (service, le))
```
#### Limit Data Points
```yaml
# Good: Reasonable resolution for dashboard
- expr: http_request_rate_5m[1h]
interval: 15s # One point every 15 seconds
# Bad: Too many points for visualization
- expr: http_request_rate_1s[1h] # 3600 points!
```
### Dashboard Performance
#### Panel Limits
- **Maximum panels per dashboard**: 20-30
- **Maximum queries per panel**: 10
- **Maximum time series per panel**: 50
#### Caching Strategy
```yaml
# Use appropriate cache headers
cache_timeout: 30 # Cache for 30 seconds on fast-changing panels
cache_timeout: 300 # Cache for 5 minutes on slow-changing panels
```
## Accessibility
### Screen Reader Support
```yaml
# Provide text alternatives for visual elements
- title: "Service Health Status"
type: stat
options:
text_mode: value_and_name # Includes both value and description
field_config:
mappings:
- options:
"1":
text: "Healthy"
color: "green"
"0":
text: "Unhealthy"
color: "red"
```
### Keyboard Navigation
- Ensure all interactive elements are keyboard accessible
- Provide logical tab order
- Include skip links for complex dashboards
### High Contrast Mode
```yaml
# Test dashboards work in high contrast mode
theme: high_contrast
colors:
- "#000000" # Pure black
- "#ffffff" # Pure white
- "#ffff00" # Pure yellow
- "#ff0000" # Pure red
```
## Testing and Validation
### Dashboard Testing Checklist
#### Functional Testing
- [ ] All panels load without errors
- [ ] Template variables filter correctly
- [ ] Time range changes update all panels
- [ ] Drill-down links work as expected
- [ ] Auto-refresh functions properly
#### Visual Testing
- [ ] Dashboard renders correctly on different screen sizes
- [ ] Colors are distinguishable and meaningful
- [ ] Text is readable at normal zoom levels
- [ ] Legends and labels are clear
#### Performance Testing
- [ ] Dashboard loads in < 5 seconds
- [ ] No queries timeout under normal load
- [ ] Auto-refresh doesn't cause browser lag
- [ ] Memory usage remains reasonable
#### Usability Testing
- [ ] New team members can understand the dashboard
- [ ] Action items are clear during incidents
- [ ] Key information is quickly discoverable
- [ ] Dashboard supports common troubleshooting workflows
## Maintenance and Governance
### Dashboard Lifecycle
#### Creation
1. Define dashboard purpose and audience
2. Identify key metrics and success criteria
3. Design layout following established patterns
4. Implement with consistent styling
5. Test with real data and user scenarios
#### Maintenance
- **Weekly**: Check for broken panels or queries
- **Monthly**: Review dashboard usage analytics
- **Quarterly**: Gather user feedback and iterate
- **Annually**: Major review and potential redesign
#### Retirement
- Archive dashboards that are no longer used
- Migrate users to replacement dashboards
- Document lessons learned
### Dashboard Standards
```yaml
# Organization dashboard standards
standards:
naming_convention: "[Team] [Service] - [Purpose]"
tags: [team, service_type, environment, purpose]
refresh_intervals: [15s, 30s, 1m, 5m, 15m]
time_ranges: [5m, 15m, 1h, 4h, 1d, 7d, 30d]
color_scheme: "company_standard"
max_panels_per_dashboard: 25
```
## Advanced Patterns
### Composite Dashboards
```yaml
# Dashboard that includes panels from other dashboards
- title: "Service Overview"
type: dashlist
targets:
- "service-health"
- "service-performance"
- "service-business-metrics"
options:
show_headings: true
max_items: 10
```
### Dynamic Dashboard Generation
```python
# Generate dashboards from service definitions
def generate_service_dashboard(service_config):
panels = []
# Always include golden signals
panels.extend(generate_golden_signals_panels(service_config))
# Add service-specific panels
if service_config.type == 'database':
panels.extend(generate_database_panels(service_config))
elif service_config.type == 'queue':
panels.extend(generate_queue_panels(service_config))
return {
'title': f"{service_config.name} - Operational Dashboard",
'panels': panels,
'variables': generate_variables(service_config)
}
```
### A/B Testing for Dashboards
```yaml
# Test different dashboard designs with different teams
experiment:
name: "dashboard_layout_test"
variants:
- name: "traditional_layout"
weight: 50
config: "dashboard_v1.json"
- name: "f_pattern_layout"
weight: 50
config: "dashboard_v2.json"
success_metrics:
- "time_to_insight"
- "user_satisfaction"
- "troubleshooting_efficiency"
```
Remember: A dashboard should tell a story about your system's health and guide users toward the right actions. Focus on clarity over complexity, and always optimize for the person who will use it during a stressful incident.
FILE:references/slo_cookbook.md
# SLO Cookbook: A Practical Guide to Service Level Objectives
## Introduction
Service Level Objectives (SLOs) are a key tool for managing service reliability. This cookbook provides practical guidance for implementing SLOs that actually improve system reliability rather than just creating meaningless metrics.
## Fundamentals
### The SLI/SLO/SLA Hierarchy
- **SLI (Service Level Indicator)**: A quantifiable measure of service quality
- **SLO (Service Level Objective)**: A target range of values for an SLI
- **SLA (Service Level Agreement)**: A business agreement with consequences for missing SLO targets
### Golden Rule of SLOs
**Start simple, iterate based on learning.** Your first SLOs won't be perfect, and that's okay.
## Choosing Good SLIs
### The Four Golden Signals
1. **Latency**: How long requests take to complete
2. **Traffic**: How many requests are coming in
3. **Errors**: How many requests are failing
4. **Saturation**: How "full" your service is
### SLI Selection Criteria
A good SLI should be:
- **Measurable**: You can collect data for it
- **Meaningful**: It reflects user experience
- **Controllable**: You can take action to improve it
- **Proportional**: Changes in the SLI reflect changes in user happiness
### Service Type Specific SLIs
#### HTTP APIs
- **Request latency**: P95 or P99 response time
- **Availability**: Proportion of successful requests (non-5xx)
- **Throughput**: Requests per second capacity
```prometheus
# Availability SLI
sum(rate(http_requests_total{code!~"5.."}[5m])) / sum(rate(http_requests_total[5m]))
# Latency SLI
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
```
#### Batch Jobs
- **Freshness**: Age of the last successful run
- **Correctness**: Proportion of jobs completing successfully
- **Throughput**: Items processed per unit time
#### Data Pipelines
- **Data freshness**: Time since last successful update
- **Data quality**: Proportion of records passing validation
- **Processing latency**: Time from ingestion to availability
### Anti-Patterns in SLI Selection
❌ **Don't use**: CPU usage, memory usage, disk space as primary SLIs
- These are symptoms, not user-facing impacts
❌ **Don't use**: Counts instead of rates or proportions
- "Number of errors" vs "Error rate"
❌ **Don't use**: Internal metrics that users don't care about
- Queue depth, cache hit rate (unless they directly impact user experience)
## Setting SLO Targets
### The Art of Target Setting
Setting SLO targets is balancing act between:
- **User happiness**: Targets should reflect acceptable user experience
- **Business value**: Tighter SLOs cost more to maintain
- **Current performance**: Targets should be achievable but aspirational
### Target Setting Strategies
#### Historical Performance Method
1. Collect 4-6 weeks of historical data
2. Calculate the worst user-visible performance in that period
3. Set your SLO slightly better than the worst acceptable performance
#### User Journey Mapping
1. Map critical user journeys
2. Identify acceptable performance for each step
3. Work backwards to component SLOs
#### Error Budget Approach
1. Decide how much unreliability you can afford
2. Set SLO targets based on acceptable error budget consumption
3. Example: 99.9% availability = 43.8 minutes downtime per month
### SLO Target Examples by Service Criticality
#### Critical Services (Revenue Impact)
- **Availability**: 99.95% - 99.99%
- **Latency (P95)**: 100-200ms
- **Error Rate**: < 0.1%
#### High Priority Services
- **Availability**: 99.9% - 99.95%
- **Latency (P95)**: 200-500ms
- **Error Rate**: < 0.5%
#### Standard Services
- **Availability**: 99.5% - 99.9%
- **Latency (P95)**: 500ms - 1s
- **Error Rate**: < 1%
## Error Budget Management
### What is an Error Budget?
Your error budget is the maximum amount of unreliability you can accumulate while still meeting your SLO. It's calculated as:
```
Error Budget = (1 - SLO) × Time Window
```
For a 99.9% availability SLO over 30 days:
```
Error Budget = (1 - 0.999) × 30 days = 0.001 × 30 days = 43.8 minutes
```
### Error Budget Policies
Define what happens when you consume your error budget:
#### Conservative Policy (High-Risk Services)
- **> 50% consumed**: Freeze non-critical feature releases
- **> 75% consumed**: Focus entirely on reliability improvements
- **> 90% consumed**: Consider emergency measures (traffic shaping, etc.)
#### Balanced Policy (Standard Services)
- **> 75% consumed**: Increase focus on reliability work
- **> 90% consumed**: Pause feature work, focus on reliability
#### Aggressive Policy (Early Stage Services)
- **> 90% consumed**: Review but continue normal operations
- **100% consumed**: Evaluate SLO appropriateness
### Burn Rate Alerting
Multi-window burn rate alerts help you catch SLO violations before they become critical:
```yaml
# Fast burn: 2% budget consumed in 1 hour
- alert: FastBurnSLOViolation
expr: (
(1 - (sum(rate(http_requests_total{code!~"5.."}[5m])) / sum(rate(http_requests_total[5m])))) > (14.4 * 0.001)
and
(1 - (sum(rate(http_requests_total{code!~"5.."}[1h])) / sum(rate(http_requests_total[1h])))) > (14.4 * 0.001)
)
for: 2m
# Slow burn: 10% budget consumed in 3 days
- alert: SlowBurnSLOViolation
expr: (
(1 - (sum(rate(http_requests_total{code!~"5.."}[6h])) / sum(rate(http_requests_total[6h])))) > (1.0 * 0.001)
and
(1 - (sum(rate(http_requests_total{code!~"5.."}[3d])) / sum(rate(http_requests_total[3d])))) > (1.0 * 0.001)
)
for: 15m
```
## Implementation Patterns
### The SLO Implementation Ladder
#### Level 1: Basic SLOs
- Choose 1-2 SLIs that matter most to users
- Set aspirational but achievable targets
- Implement basic alerting when SLOs are missed
#### Level 2: Operational SLOs
- Add burn rate alerting
- Create error budget dashboards
- Establish error budget policies
- Regular SLO review meetings
#### Level 3: Advanced SLOs
- Multi-window burn rate alerts
- Automated error budget policy enforcement
- SLO-driven incident prioritization
- Integration with CI/CD for deployment decisions
### SLO Measurement Architecture
#### Push vs Pull Metrics
- **Pull** (Prometheus): Good for infrastructure metrics, real-time alerting
- **Push** (StatsD): Good for application metrics, business events
#### Measurement Points
- **Server-side**: More reliable, easier to implement
- **Client-side**: Better reflects user experience
- **Synthetic**: Consistent, predictable, may not reflect real user experience
### SLO Dashboard Design
Essential elements for SLO dashboards:
1. **Current SLO Achievement**: Large, prominent display
2. **Error Budget Remaining**: Visual indicator (gauge, progress bar)
3. **Burn Rate**: Time series showing error budget consumption rate
4. **Historical Trends**: 4-week view of SLO achievement
5. **Alerts**: Current and recent SLO-related alerts
## Advanced Topics
### Dependency SLOs
For services with dependencies:
```
SLO_service ≤ min(SLO_inherent, ∏SLO_dependencies)
```
If your service depends on 3 other services each with 99.9% SLO:
```
Maximum_SLO = 0.999³ = 0.997 = 99.7%
```
### User Journey SLOs
Track end-to-end user experiences:
```prometheus
# Registration success rate
sum(rate(user_registration_success_total[5m])) / sum(rate(user_registration_attempts_total[5m]))
# Purchase completion latency
histogram_quantile(0.95, rate(purchase_completion_duration_seconds_bucket[5m]))
```
### SLOs for Batch Systems
Special considerations for non-request/response systems:
#### Freshness SLO
```prometheus
# Data should be no more than 4 hours old
(time() - last_successful_update_timestamp) < (4 * 3600)
```
#### Throughput SLO
```prometheus
# Should process at least 1000 items per hour
rate(items_processed_total[1h]) >= 1000
```
#### Quality SLO
```prometheus
# At least 99.5% of records should pass validation
sum(rate(records_valid_total[5m])) / sum(rate(records_processed_total[5m])) >= 0.995
```
## Common Mistakes and How to Avoid Them
### Mistake 1: Too Many SLOs
**Problem**: Drowning in metrics, losing focus
**Solution**: Start with 1-2 SLOs per service, add more only when needed
### Mistake 2: Internal Metrics as SLIs
**Problem**: Optimizing for metrics that don't impact users
**Solution**: Always ask "If this metric changes, do users notice?"
### Mistake 3: Perfectionist SLOs
**Problem**: 99.99% SLO when 99.9% would be fine
**Solution**: Higher SLOs cost exponentially more; pick the minimum acceptable level
### Mistake 4: Ignoring Error Budgets
**Problem**: Treating any SLO miss as an emergency
**Solution**: Error budgets exist to be spent; use them to balance feature velocity and reliability
### Mistake 5: Static SLOs
**Problem**: Setting SLOs once and never updating them
**Solution**: Review SLOs quarterly; adjust based on user feedback and business changes
## SLO Review Process
### Monthly SLO Review Agenda
1. **SLO Achievement Review**: Did we meet our SLOs?
2. **Error Budget Analysis**: How did we spend our error budget?
3. **Incident Correlation**: Which incidents impacted our SLOs?
4. **SLI Quality Assessment**: Are our SLIs still meaningful?
5. **Target Adjustment**: Should we change any targets?
### Quarterly SLO Health Check
1. **User Impact Validation**: Survey users about acceptable performance
2. **Business Alignment**: Do SLOs still reflect business priorities?
3. **Measurement Quality**: Are we measuring the right things?
4. **Cost/Benefit Analysis**: Are tighter SLOs worth the investment?
## Tooling and Automation
### Essential Tools
1. **Metrics Collection**: Prometheus, InfluxDB, CloudWatch
2. **Alerting**: Alertmanager, PagerDuty, OpsGenie
3. **Dashboards**: Grafana, DataDog, New Relic
4. **SLO Platforms**: Sloth, Pyrra, Service Level Blue
### Automation Opportunities
- **Burn rate alert generation** from SLO definitions
- **Dashboard creation** from SLO specifications
- **Error budget calculation** and tracking
- **Release blocking** based on error budget consumption
## Getting Started Checklist
- [ ] Identify your service's critical user journeys
- [ ] Choose 1-2 SLIs that best reflect user experience
- [ ] Collect 4-6 weeks of baseline data
- [ ] Set initial SLO targets based on historical performance
- [ ] Implement basic SLO monitoring and alerting
- [ ] Create an SLO dashboard
- [ ] Define error budget policies
- [ ] Schedule monthly SLO reviews
- [ ] Plan for quarterly SLO health checks
Remember: SLOs are a journey, not a destination. Start simple, learn from experience, and iterate toward better reliability management.
FILE:scripts/alert_optimizer.py
#!/usr/bin/env python3
"""
Alert Optimizer - Analyze and optimize alert configurations
This script analyzes existing alert configurations and identifies optimization opportunities:
- Noisy alerts with high false positive rates
- Missing coverage gaps in monitoring
- Duplicate or redundant alerts
- Poor threshold settings and alert fatigue risks
- Missing runbooks and documentation
- Routing and escalation policy improvements
Usage:
python alert_optimizer.py --input alert_config.json --output optimized_config.json
python alert_optimizer.py --input alerts.json --analyze-only --report report.html
"""
import json
import argparse
import sys
import re
import math
from typing import Dict, List, Any, Tuple, Set
from datetime import datetime, timedelta
from collections import defaultdict, Counter
class AlertOptimizer:
"""Analyze and optimize alert configurations."""
# Alert severity priority mapping
SEVERITY_PRIORITY = {
'critical': 1,
'high': 2,
'warning': 3,
'info': 4
}
# Common noisy alert patterns
NOISY_PATTERNS = [
r'disk.*usage.*>.*[89]\d%', # Disk usage > 80% often noisy
r'memory.*>.*[89]\d%', # Memory > 80% often noisy
r'cpu.*>.*[789]\d%', # CPU > 70% can be noisy
r'response.*time.*>.*\d+ms', # Low latency thresholds
r'error.*rate.*>.*0\.[01]%' # Very low error rate thresholds
]
# Essential monitoring categories
COVERAGE_CATEGORIES = [
'availability',
'latency',
'error_rate',
'resource_utilization',
'security',
'business_metrics'
]
# Golden signals that should always be monitored
GOLDEN_SIGNALS = [
'latency',
'traffic',
'errors',
'saturation'
]
def __init__(self):
"""Initialize the Alert Optimizer."""
self.alert_config = {}
self.optimization_results = {}
self.alert_analysis = {}
def load_alert_config(self, file_path: str) -> Dict[str, Any]:
"""Load alert configuration from JSON file."""
try:
with open(file_path, 'r') as f:
return json.load(f)
except FileNotFoundError:
raise ValueError(f"Alert configuration file not found: {file_path}")
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in alert configuration: {e}")
def analyze_alert_noise(self, alerts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Identify potentially noisy alerts."""
noisy_alerts = []
for alert in alerts:
noise_score = 0
noise_reasons = []
alert_rule = alert.get('expr', alert.get('condition', ''))
alert_name = alert.get('alert', alert.get('name', 'Unknown'))
# Check for common noisy patterns
for pattern in self.NOISY_PATTERNS:
if re.search(pattern, alert_rule, re.IGNORECASE):
noise_score += 3
noise_reasons.append(f"Matches noisy pattern: {pattern}")
# Check for very frequent evaluation intervals
evaluation_interval = alert.get('for', '0s')
if self._parse_duration(evaluation_interval) < 60: # Less than 1 minute
noise_score += 2
noise_reasons.append("Very short evaluation interval")
# Check for lack of 'for' clause
if not alert.get('for') or alert.get('for') == '0s':
noise_score += 2
noise_reasons.append("No 'for' clause - may cause alert flapping")
# Check for overly sensitive thresholds
if self._has_sensitive_threshold(alert_rule):
noise_score += 2
noise_reasons.append("Potentially sensitive threshold")
# Check historical firing rate if available
historical_data = alert.get('historical_data', {})
if historical_data:
firing_rate = historical_data.get('fires_per_day', 0)
if firing_rate > 10: # More than 10 fires per day
noise_score += 3
noise_reasons.append(f"High firing rate: {firing_rate} times/day")
false_positive_rate = historical_data.get('false_positive_rate', 0)
if false_positive_rate > 0.3: # > 30% false positives
noise_score += 4
noise_reasons.append(f"High false positive rate: {false_positive_rate*100:.1f}%")
if noise_score >= 3: # Threshold for considering an alert noisy
noisy_alert = {
'alert_name': alert_name,
'noise_score': noise_score,
'reasons': noise_reasons,
'current_rule': alert_rule,
'recommendations': self._generate_noise_reduction_recommendations(alert, noise_reasons)
}
noisy_alerts.append(noisy_alert)
return sorted(noisy_alerts, key=lambda x: x['noise_score'], reverse=True)
def _parse_duration(self, duration_str: str) -> int:
"""Parse duration string to seconds."""
if not duration_str or duration_str == '0s':
return 0
duration_map = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}
match = re.match(r'(\d+)([smhd])', duration_str)
if match:
value, unit = match.groups()
return int(value) * duration_map.get(unit, 1)
return 0
def _has_sensitive_threshold(self, rule: str) -> bool:
"""Check if alert rule has potentially sensitive thresholds."""
# Look for very low error rates or very tight latency thresholds
sensitive_patterns = [
r'error.*rate.*>.*0\.0[01]', # Error rate > 0.01% or 0.001%
r'latency.*>.*[12]\d\d?ms', # Latency > 100-299ms
r'response.*time.*>.*0\.[12]', # Response time > 0.1-0.2s
r'cpu.*>.*[456]\d%' # CPU > 40-69% (too sensitive for most cases)
]
for pattern in sensitive_patterns:
if re.search(pattern, rule, re.IGNORECASE):
return True
return False
def _generate_noise_reduction_recommendations(self, alert: Dict[str, Any],
reasons: List[str]) -> List[str]:
"""Generate recommendations to reduce alert noise."""
recommendations = []
if "No 'for' clause" in str(reasons):
recommendations.append("Add 'for: 5m' clause to prevent flapping")
if "Very short evaluation interval" in str(reasons):
recommendations.append("Increase evaluation interval to at least 1 minute")
if "sensitive threshold" in str(reasons):
recommendations.append("Review and increase threshold based on historical data")
if "High firing rate" in str(reasons):
recommendations.append("Analyze historical firing patterns and adjust thresholds")
if "High false positive rate" in str(reasons):
recommendations.append("Implement more specific conditions to reduce false positives")
if "noisy pattern" in str(reasons):
recommendations.append("Consider using percentile-based thresholds instead of absolute values")
return recommendations
def identify_coverage_gaps(self, alerts: List[Dict[str, Any]],
services: List[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Identify gaps in monitoring coverage."""
coverage_analysis = {
'missing_categories': [],
'missing_golden_signals': [],
'service_coverage_gaps': [],
'critical_gaps': [],
'recommendations': []
}
# Analyze coverage by category
covered_categories = set()
alert_categories = []
for alert in alerts:
alert_rule = alert.get('expr', alert.get('condition', ''))
alert_name = alert.get('alert', alert.get('name', ''))
category = self._classify_alert_category(alert_rule, alert_name)
if category:
covered_categories.add(category)
alert_categories.append(category)
# Check for missing essential categories
missing_categories = set(self.COVERAGE_CATEGORIES) - covered_categories
coverage_analysis['missing_categories'] = list(missing_categories)
# Check for missing golden signals
covered_signals = set()
for alert in alerts:
alert_rule = alert.get('expr', alert.get('condition', ''))
signal = self._identify_golden_signal(alert_rule)
if signal:
covered_signals.add(signal)
missing_signals = set(self.GOLDEN_SIGNALS) - covered_signals
coverage_analysis['missing_golden_signals'] = list(missing_signals)
# Analyze service-specific coverage if service list provided
if services:
service_coverage = self._analyze_service_coverage(alerts, services)
coverage_analysis['service_coverage_gaps'] = service_coverage
# Identify critical gaps
critical_gaps = []
if 'availability' in missing_categories:
critical_gaps.append("Missing availability monitoring")
if 'error_rate' in missing_categories:
critical_gaps.append("Missing error rate monitoring")
if 'errors' in missing_signals:
critical_gaps.append("Missing error signal monitoring")
coverage_analysis['critical_gaps'] = critical_gaps
# Generate recommendations
recommendations = self._generate_coverage_recommendations(coverage_analysis)
coverage_analysis['recommendations'] = recommendations
return coverage_analysis
def _classify_alert_category(self, rule: str, alert_name: str) -> str:
"""Classify alert into monitoring category."""
rule_lower = rule.lower()
name_lower = alert_name.lower()
if any(keyword in rule_lower or keyword in name_lower
for keyword in ['up', 'down', 'available', 'reachable']):
return 'availability'
if any(keyword in rule_lower or keyword in name_lower
for keyword in ['latency', 'response_time', 'duration']):
return 'latency'
if any(keyword in rule_lower or keyword in name_lower
for keyword in ['error', 'fail', '5xx', '4xx']):
return 'error_rate'
if any(keyword in rule_lower or keyword in name_lower
for keyword in ['cpu', 'memory', 'disk', 'network', 'utilization']):
return 'resource_utilization'
if any(keyword in rule_lower or keyword in name_lower
for keyword in ['security', 'auth', 'login', 'breach']):
return 'security'
if any(keyword in rule_lower or keyword in name_lower
for keyword in ['revenue', 'conversion', 'user', 'business']):
return 'business_metrics'
return 'other'
def _identify_golden_signal(self, rule: str) -> str:
"""Identify which golden signal an alert covers."""
rule_lower = rule.lower()
if any(keyword in rule_lower for keyword in ['latency', 'response_time', 'duration']):
return 'latency'
if any(keyword in rule_lower for keyword in ['rate', 'rps', 'qps', 'throughput']):
return 'traffic'
if any(keyword in rule_lower for keyword in ['error', 'fail', '5xx']):
return 'errors'
if any(keyword in rule_lower for keyword in ['cpu', 'memory', 'disk', 'utilization']):
return 'saturation'
return None
def _analyze_service_coverage(self, alerts: List[Dict[str, Any]],
services: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Analyze monitoring coverage per service."""
service_coverage = []
for service in services:
service_name = service.get('name', '')
service_alerts = [alert for alert in alerts
if service_name in alert.get('expr', '') or
service_name in alert.get('labels', {}).get('service', '')]
covered_signals = set()
for alert in service_alerts:
signal = self._identify_golden_signal(alert.get('expr', ''))
if signal:
covered_signals.add(signal)
missing_signals = set(self.GOLDEN_SIGNALS) - covered_signals
if missing_signals or len(service_alerts) < 3: # Less than 3 alerts per service
coverage_gap = {
'service': service_name,
'alert_count': len(service_alerts),
'covered_signals': list(covered_signals),
'missing_signals': list(missing_signals),
'criticality': service.get('criticality', 'medium'),
'recommendations': []
}
if len(service_alerts) == 0:
coverage_gap['recommendations'].append("Add basic availability monitoring")
if 'errors' in missing_signals:
coverage_gap['recommendations'].append("Add error rate monitoring")
if 'latency' in missing_signals:
coverage_gap['recommendations'].append("Add latency monitoring")
service_coverage.append(coverage_gap)
return service_coverage
def _generate_coverage_recommendations(self, coverage_analysis: Dict[str, Any]) -> List[str]:
"""Generate recommendations to improve monitoring coverage."""
recommendations = []
for missing_category in coverage_analysis['missing_categories']:
if missing_category == 'availability':
recommendations.append("Add service availability/uptime monitoring")
elif missing_category == 'latency':
recommendations.append("Add response time and latency monitoring")
elif missing_category == 'error_rate':
recommendations.append("Add error rate and HTTP status code monitoring")
elif missing_category == 'resource_utilization':
recommendations.append("Add CPU, memory, and disk utilization monitoring")
elif missing_category == 'security':
recommendations.append("Add security monitoring (auth failures, suspicious activity)")
elif missing_category == 'business_metrics':
recommendations.append("Add business KPI monitoring")
for missing_signal in coverage_analysis['missing_golden_signals']:
recommendations.append(f"Implement {missing_signal} monitoring (Golden Signal)")
if coverage_analysis['critical_gaps']:
recommendations.append("Address critical monitoring gaps as highest priority")
return recommendations
def find_duplicate_alerts(self, alerts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Identify duplicate or redundant alerts."""
duplicates = []
alert_signatures = defaultdict(list)
# Group alerts by signature
for i, alert in enumerate(alerts):
signature = self._generate_alert_signature(alert)
alert_signatures[signature].append((i, alert))
# Find exact duplicates
for signature, alert_group in alert_signatures.items():
if len(alert_group) > 1:
duplicate_group = {
'type': 'exact_duplicate',
'signature': signature,
'alerts': [{'index': i, 'name': alert.get('alert', alert.get('name', f'Alert_{i}'))}
for i, alert in alert_group],
'recommendation': 'Remove duplicate alerts, keep the most comprehensive one'
}
duplicates.append(duplicate_group)
# Find semantic duplicates (similar but not identical)
semantic_duplicates = self._find_semantic_duplicates(alerts)
duplicates.extend(semantic_duplicates)
return duplicates
def _generate_alert_signature(self, alert: Dict[str, Any]) -> str:
"""Generate a signature for alert comparison."""
expr = alert.get('expr', alert.get('condition', ''))
labels = alert.get('labels', {})
# Normalize the expression by removing whitespace and standardizing
normalized_expr = re.sub(r'\s+', ' ', expr).strip()
# Create signature from expression and key labels
key_labels = {k: v for k, v in labels.items()
if k in ['service', 'severity', 'team']}
return f"{normalized_expr}::{json.dumps(key_labels, sort_keys=True)}"
def _find_semantic_duplicates(self, alerts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Find semantically similar alerts."""
semantic_duplicates = []
# Group alerts by service and metric type
service_groups = defaultdict(list)
for i, alert in enumerate(alerts):
service = self._extract_service_from_alert(alert)
metric_type = self._extract_metric_type_from_alert(alert)
key = f"{service}::{metric_type}"
service_groups[key].append((i, alert))
# Look for similar alerts within each group
for key, alert_group in service_groups.items():
if len(alert_group) > 1:
similar_alerts = self._identify_similar_alerts(alert_group)
if similar_alerts:
semantic_duplicates.extend(similar_alerts)
return semantic_duplicates
def _extract_service_from_alert(self, alert: Dict[str, Any]) -> str:
"""Extract service name from alert."""
labels = alert.get('labels', {})
if 'service' in labels:
return labels['service']
expr = alert.get('expr', alert.get('condition', ''))
# Try to extract service from metric labels
service_match = re.search(r'service="([^"]+)"', expr)
if service_match:
return service_match.group(1)
return 'unknown'
def _extract_metric_type_from_alert(self, alert: Dict[str, Any]) -> str:
"""Extract metric type from alert."""
expr = alert.get('expr', alert.get('condition', ''))
# Common metric patterns
if 'up' in expr.lower():
return 'availability'
elif any(keyword in expr.lower() for keyword in ['latency', 'duration', 'response_time']):
return 'latency'
elif any(keyword in expr.lower() for keyword in ['error', 'fail', '5xx']):
return 'error_rate'
elif any(keyword in expr.lower() for keyword in ['cpu', 'memory', 'disk']):
return 'resource'
return 'other'
def _identify_similar_alerts(self, alert_group: List[Tuple[int, Dict[str, Any]]]) -> List[Dict[str, Any]]:
"""Identify similar alerts within a group."""
similar_groups = []
# Simple similarity check based on threshold values and conditions
threshold_groups = defaultdict(list)
for index, alert in alert_group:
expr = alert.get('expr', alert.get('condition', ''))
threshold = self._extract_threshold_from_expression(expr)
severity = alert.get('labels', {}).get('severity', 'unknown')
similarity_key = f"{threshold}::{severity}"
threshold_groups[similarity_key].append((index, alert))
# If multiple alerts have very similar thresholds, they might be redundant
for similarity_key, similar_alerts in threshold_groups.items():
if len(similar_alerts) > 1:
similar_group = {
'type': 'semantic_duplicate',
'similarity_key': similarity_key,
'alerts': [{'index': i, 'name': alert.get('alert', alert.get('name', f'Alert_{i}'))}
for i, alert in similar_alerts],
'recommendation': 'Review for potential consolidation - similar thresholds and conditions'
}
similar_groups.append(similar_group)
return similar_groups
def _extract_threshold_from_expression(self, expr: str) -> str:
"""Extract threshold value from alert expression."""
# Look for common threshold patterns
threshold_patterns = [
r'>[\s]*([0-9.]+)',
r'<[\s]*([0-9.]+)',
r'>=[\s]*([0-9.]+)',
r'<=[\s]*([0-9.]+)',
r'==[\s]*([0-9.]+)'
]
for pattern in threshold_patterns:
match = re.search(pattern, expr)
if match:
return match.group(1)
return 'unknown'
def analyze_thresholds(self, alerts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Analyze alert thresholds for optimization opportunities."""
threshold_analysis = []
for alert in alerts:
alert_name = alert.get('alert', alert.get('name', 'Unknown'))
expr = alert.get('expr', alert.get('condition', ''))
analysis = {
'alert_name': alert_name,
'current_expression': expr,
'threshold_issues': [],
'recommendations': []
}
# Check for hard-coded thresholds
if re.search(r'[><=]\s*[0-9.]+', expr):
analysis['threshold_issues'].append('Hard-coded threshold value')
analysis['recommendations'].append('Consider parameterizing thresholds')
# Check for percentage-based thresholds that might be too strict
percentage_match = re.search(r'([><=])\s*0?\.\d+', expr)
if percentage_match:
operator = percentage_match.group(1)
if operator in ['>', '>='] and 'error' in expr.lower():
analysis['threshold_issues'].append('Very low error rate threshold')
analysis['recommendations'].append('Consider increasing error rate threshold based on SLO')
# Check for missing hysteresis
if '>' in expr and 'for:' not in str(alert):
analysis['threshold_issues'].append('No hysteresis (for clause)')
analysis['recommendations'].append('Add "for" clause to prevent alert flapping')
# Check for resource utilization thresholds
if any(resource in expr.lower() for resource in ['cpu', 'memory', 'disk']):
threshold_value = self._extract_threshold_from_expression(expr)
if threshold_value and threshold_value.replace('.', '').isdigit():
threshold_num = float(threshold_value)
if threshold_num < 0.7: # Less than 70%
analysis['threshold_issues'].append('Low resource utilization threshold')
analysis['recommendations'].append('Consider increasing threshold to reduce noise')
# Add historical data analysis if available
historical_data = alert.get('historical_data', {})
if historical_data:
false_positive_rate = historical_data.get('false_positive_rate', 0)
if false_positive_rate > 0.2:
analysis['threshold_issues'].append(f'High false positive rate: {false_positive_rate*100:.1f}%')
analysis['recommendations'].append('Analyze historical data and adjust threshold')
if analysis['threshold_issues']:
threshold_analysis.append(analysis)
return threshold_analysis
def assess_alert_fatigue_risk(self, alerts: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Assess risk of alert fatigue."""
fatigue_assessment = {
'total_alerts': len(alerts),
'risk_level': 'low',
'risk_factors': [],
'metrics': {},
'recommendations': []
}
# Count alerts by severity
severity_counts = Counter()
for alert in alerts:
severity = alert.get('labels', {}).get('severity', 'unknown')
severity_counts[severity] += 1
fatigue_assessment['metrics']['severity_distribution'] = dict(severity_counts)
# Calculate risk factors
critical_count = severity_counts.get('critical', 0)
warning_count = severity_counts.get('warning', 0) + severity_counts.get('high', 0)
total_high_priority = critical_count + warning_count
# Too many high-priority alerts
if total_high_priority > 50:
fatigue_assessment['risk_factors'].append('High number of critical/warning alerts')
fatigue_assessment['recommendations'].append('Review and reduce number of high-priority alerts')
# Poor critical to warning ratio
if critical_count > 0 and warning_count > 0:
critical_ratio = critical_count / (critical_count + warning_count)
if critical_ratio > 0.3: # More than 30% critical
fatigue_assessment['risk_factors'].append('High ratio of critical alerts')
fatigue_assessment['recommendations'].append('Review critical alert criteria - not everything should be critical')
# Estimate daily alert volume
daily_estimate = self._estimate_daily_alert_volume(alerts)
fatigue_assessment['metrics']['estimated_daily_alerts'] = daily_estimate
if daily_estimate > 100:
fatigue_assessment['risk_factors'].append('High estimated daily alert volume')
fatigue_assessment['recommendations'].append('Implement alert grouping and suppression rules')
# Check for missing runbooks
alerts_without_runbooks = [alert for alert in alerts
if not alert.get('annotations', {}).get('runbook_url')]
runbook_ratio = len(alerts_without_runbooks) / len(alerts) if alerts else 0
if runbook_ratio > 0.5:
fatigue_assessment['risk_factors'].append('Many alerts lack runbooks')
fatigue_assessment['recommendations'].append('Create runbooks for alerts to improve response efficiency')
# Determine overall risk level
risk_score = len(fatigue_assessment['risk_factors'])
if risk_score >= 3:
fatigue_assessment['risk_level'] = 'high'
elif risk_score >= 1:
fatigue_assessment['risk_level'] = 'medium'
return fatigue_assessment
def _estimate_daily_alert_volume(self, alerts: List[Dict[str, Any]]) -> int:
"""Estimate daily alert volume."""
total_estimated = 0
for alert in alerts:
# Use historical data if available
historical_data = alert.get('historical_data', {})
if historical_data and 'fires_per_day' in historical_data:
total_estimated += historical_data['fires_per_day']
continue
# Otherwise estimate based on alert characteristics
expr = alert.get('expr', alert.get('condition', ''))
severity = alert.get('labels', {}).get('severity', 'warning')
# Base estimate by severity
base_estimates = {
'critical': 0.1, # Critical should rarely fire
'high': 0.5,
'warning': 2,
'info': 5
}
estimate = base_estimates.get(severity, 1)
# Adjust based on alert type
if 'error_rate' in expr.lower():
estimate *= 1.5 # Error rate alerts tend to be more frequent
elif 'availability' in expr.lower() or 'up' in expr.lower():
estimate *= 0.5 # Availability alerts should be rare
total_estimated += estimate
return int(total_estimated)
def generate_optimized_config(self, alerts: List[Dict[str, Any]],
analysis_results: Dict[str, Any]) -> Dict[str, Any]:
"""Generate optimized alert configuration."""
optimized_alerts = []
for i, alert in enumerate(alerts):
optimized_alert = alert.copy()
alert_name = alert.get('alert', alert.get('name', f'Alert_{i}'))
# Apply noise reduction optimizations
noisy_alerts = analysis_results.get('noisy_alerts', [])
for noisy_alert in noisy_alerts:
if noisy_alert['alert_name'] == alert_name:
optimized_alert = self._apply_noise_reduction(optimized_alert, noisy_alert)
break
# Apply threshold optimizations
threshold_issues = analysis_results.get('threshold_analysis', [])
for threshold_issue in threshold_issues:
if threshold_issue['alert_name'] == alert_name:
optimized_alert = self._apply_threshold_optimization(optimized_alert, threshold_issue)
break
# Ensure proper alert metadata
optimized_alert = self._ensure_alert_metadata(optimized_alert)
optimized_alerts.append(optimized_alert)
# Remove duplicates based on analysis
if 'duplicate_alerts' in analysis_results:
optimized_alerts = self._remove_duplicate_alerts(optimized_alerts,
analysis_results['duplicate_alerts'])
# Add missing alerts for coverage gaps
if 'coverage_gaps' in analysis_results:
new_alerts = self._generate_missing_alerts(analysis_results['coverage_gaps'])
optimized_alerts.extend(new_alerts)
optimized_config = {
'alerts': optimized_alerts,
'optimization_metadata': {
'optimized_at': datetime.utcnow().isoformat() + 'Z',
'original_count': len(alerts),
'optimized_count': len(optimized_alerts),
'changes_applied': analysis_results.get('optimizations_applied', [])
}
}
return optimized_config
def _apply_noise_reduction(self, alert: Dict[str, Any],
noise_analysis: Dict[str, Any]) -> Dict[str, Any]:
"""Apply noise reduction optimizations to an alert."""
optimized_alert = alert.copy()
for recommendation in noise_analysis['recommendations']:
if 'for:' in recommendation and not alert.get('for'):
optimized_alert['for'] = '5m'
elif 'threshold' in recommendation.lower():
# This would require more sophisticated threshold adjustment
# For now, add annotation for manual review
if 'annotations' not in optimized_alert:
optimized_alert['annotations'] = {}
optimized_alert['annotations']['optimization_note'] = 'Review threshold - potentially too sensitive'
return optimized_alert
def _apply_threshold_optimization(self, alert: Dict[str, Any],
threshold_analysis: Dict[str, Any]) -> Dict[str, Any]:
"""Apply threshold optimizations to an alert."""
optimized_alert = alert.copy()
# Add 'for' clause if missing
if 'No hysteresis' in str(threshold_analysis['threshold_issues']):
if not alert.get('for'):
optimized_alert['for'] = '5m'
# Add optimization annotations
if threshold_analysis['recommendations']:
if 'annotations' not in optimized_alert:
optimized_alert['annotations'] = {}
optimized_alert['annotations']['threshold_recommendations'] = '; '.join(threshold_analysis['recommendations'])
return optimized_alert
def _ensure_alert_metadata(self, alert: Dict[str, Any]) -> Dict[str, Any]:
"""Ensure alert has proper metadata."""
optimized_alert = alert.copy()
# Ensure annotations exist
if 'annotations' not in optimized_alert:
optimized_alert['annotations'] = {}
# Add summary if missing
if 'summary' not in optimized_alert['annotations']:
alert_name = alert.get('alert', alert.get('name', 'Alert'))
optimized_alert['annotations']['summary'] = f"Alert: {alert_name}"
# Add description if missing
if 'description' not in optimized_alert['annotations']:
optimized_alert['annotations']['description'] = 'This alert requires a description. Please update with specific details about the condition and impact.'
# Ensure proper labels
if 'labels' not in optimized_alert:
optimized_alert['labels'] = {}
if 'severity' not in optimized_alert['labels']:
optimized_alert['labels']['severity'] = 'warning'
return optimized_alert
def _remove_duplicate_alerts(self, alerts: List[Dict[str, Any]],
duplicates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Remove duplicate alerts from the list."""
indices_to_remove = set()
for duplicate_group in duplicates:
if duplicate_group['type'] == 'exact_duplicate':
# Keep the first alert, remove the rest
alert_indices = [alert_info['index'] for alert_info in duplicate_group['alerts']]
indices_to_remove.update(alert_indices[1:]) # Remove all but first
return [alert for i, alert in enumerate(alerts) if i not in indices_to_remove]
def _generate_missing_alerts(self, coverage_gaps: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Generate alerts for missing coverage."""
new_alerts = []
for missing_signal in coverage_gaps.get('missing_golden_signals', []):
if missing_signal == 'latency':
new_alert = {
'alert': 'HighLatency',
'expr': 'histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 0.5',
'for': '5m',
'labels': {
'severity': 'warning'
},
'annotations': {
'summary': 'High request latency detected',
'description': 'The 95th percentile latency is above 500ms for 5 minutes.',
'generated': 'true'
}
}
new_alerts.append(new_alert)
elif missing_signal == 'errors':
new_alert = {
'alert': 'HighErrorRate',
'expr': 'sum(rate(http_requests_total{code=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.01',
'for': '5m',
'labels': {
'severity': 'warning'
},
'annotations': {
'summary': 'High error rate detected',
'description': 'Error rate is above 1% for 5 minutes.',
'generated': 'true'
}
}
new_alerts.append(new_alert)
return new_alerts
def analyze_configuration(self, alert_config: Dict[str, Any]) -> Dict[str, Any]:
"""Perform comprehensive analysis of alert configuration."""
alerts = alert_config.get('alerts', alert_config.get('rules', []))
services = alert_config.get('services', [])
analysis_results = {
'summary': {
'total_alerts': len(alerts),
'analysis_timestamp': datetime.utcnow().isoformat() + 'Z'
},
'noisy_alerts': self.analyze_alert_noise(alerts),
'coverage_gaps': self.identify_coverage_gaps(alerts, services),
'duplicate_alerts': self.find_duplicate_alerts(alerts),
'threshold_analysis': self.analyze_thresholds(alerts),
'alert_fatigue_assessment': self.assess_alert_fatigue_risk(alerts)
}
# Generate overall recommendations
analysis_results['overall_recommendations'] = self._generate_overall_recommendations(analysis_results)
return analysis_results
def _generate_overall_recommendations(self, analysis_results: Dict[str, Any]) -> List[str]:
"""Generate overall recommendations based on complete analysis."""
recommendations = []
# High-priority recommendations
if analysis_results['alert_fatigue_assessment']['risk_level'] == 'high':
recommendations.append("HIGH PRIORITY: Address alert fatigue risk by reducing alert volume")
if len(analysis_results['coverage_gaps']['critical_gaps']) > 0:
recommendations.append("HIGH PRIORITY: Address critical monitoring gaps")
# Medium-priority recommendations
if len(analysis_results['noisy_alerts']) > 0:
recommendations.append(f"Optimize {len(analysis_results['noisy_alerts'])} noisy alerts to reduce false positives")
if len(analysis_results['duplicate_alerts']) > 0:
recommendations.append(f"Remove or consolidate {len(analysis_results['duplicate_alerts'])} duplicate alert groups")
# General recommendations
recommendations.append("Implement proper alert routing and escalation policies")
recommendations.append("Create runbooks for all production alerts")
recommendations.append("Set up alert effectiveness monitoring and regular reviews")
return recommendations
def export_analysis(self, analysis_results: Dict[str, Any], output_file: str,
format_type: str = 'json'):
"""Export analysis results."""
if format_type.lower() == 'json':
with open(output_file, 'w') as f:
json.dump(analysis_results, f, indent=2)
elif format_type.lower() == 'html':
self._export_html_report(analysis_results, output_file)
else:
raise ValueError(f"Unsupported format: {format_type}")
def _export_html_report(self, analysis_results: Dict[str, Any], output_file: str):
"""Export analysis as HTML report."""
html_content = self._generate_html_report(analysis_results)
with open(output_file, 'w') as f:
f.write(html_content)
def _generate_html_report(self, analysis_results: Dict[str, Any]) -> str:
"""Generate HTML report of analysis results."""
html = f"""
<!DOCTYPE html>
<html>
<head>
<title>Alert Configuration Analysis Report</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
.header {{ background: #f4f4f4; padding: 20px; border-radius: 5px; }}
.section {{ margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }}
.critical {{ border-left: 5px solid #ff0000; }}
.warning {{ border-left: 5px solid #ff9900; }}
.info {{ border-left: 5px solid #0066cc; }}
.success {{ border-left: 5px solid #00aa00; }}
ul {{ margin: 10px 0; }}
li {{ margin: 5px 0; }}
</style>
</head>
<body>
<div class="header">
<h1>Alert Configuration Analysis Report</h1>
<p>Generated: {analysis_results['summary']['analysis_timestamp']}</p>
<p>Total Alerts Analyzed: {analysis_results['summary']['total_alerts']}</p>
</div>
<div class="section critical">
<h2>Overall Recommendations</h2>
<ul>
{''.join(f'<li>{rec}</li>' for rec in analysis_results['overall_recommendations'])}
</ul>
</div>
<div class="section warning">
<h2>Alert Fatigue Assessment</h2>
<p><strong>Risk Level:</strong> {analysis_results['alert_fatigue_assessment']['risk_level'].upper()}</p>
<p><strong>Risk Factors:</strong></p>
<ul>
{''.join(f'<li>{factor}</li>' for factor in analysis_results['alert_fatigue_assessment']['risk_factors'])}
</ul>
</div>
<div class="section info">
<h2>Noisy Alerts ({len(analysis_results['noisy_alerts'])})</h2>
{''.join(f'<div><strong>{alert["alert_name"]}</strong> (Score: {alert["noise_score"]})<ul>{"".join(f"<li>{reason}</li>" for reason in alert["reasons"])}</ul></div>'
for alert in analysis_results['noisy_alerts'][:5])}
</div>
<div class="section info">
<h2>Coverage Gaps</h2>
<p><strong>Missing Categories:</strong> {', '.join(analysis_results['coverage_gaps']['missing_categories']) or 'None'}</p>
<p><strong>Missing Golden Signals:</strong> {', '.join(analysis_results['coverage_gaps']['missing_golden_signals']) or 'None'}</p>
<p><strong>Critical Gaps:</strong> {len(analysis_results['coverage_gaps']['critical_gaps'])}</p>
</div>
</body>
</html>
"""
return html
def print_summary(self, analysis_results: Dict[str, Any]):
"""Print human-readable summary of analysis."""
print(f"\n{'='*60}")
print(f"ALERT CONFIGURATION ANALYSIS SUMMARY")
print(f"{'='*60}")
summary = analysis_results['summary']
print(f"\nOverall Statistics:")
print(f" Total Alerts: {summary['total_alerts']}")
print(f" Analysis Date: {summary['analysis_timestamp']}")
# Alert fatigue assessment
fatigue = analysis_results['alert_fatigue_assessment']
print(f"\nAlert Fatigue Risk: {fatigue['risk_level'].upper()}")
if fatigue['risk_factors']:
print(f" Risk Factors:")
for factor in fatigue['risk_factors']:
print(f" • {factor}")
# Noisy alerts
noisy = analysis_results['noisy_alerts']
print(f"\nNoisy Alerts: {len(noisy)}")
if noisy:
print(f" Top 3 Noisiest:")
for alert in noisy[:3]:
print(f" • {alert['alert_name']} (Score: {alert['noise_score']})")
# Coverage gaps
gaps = analysis_results['coverage_gaps']
print(f"\nMonitoring Coverage:")
print(f" Missing Categories: {len(gaps['missing_categories'])}")
print(f" Missing Golden Signals: {len(gaps['missing_golden_signals'])}")
print(f" Critical Gaps: {len(gaps['critical_gaps'])}")
# Duplicates
duplicates = analysis_results['duplicate_alerts']
print(f"\nDuplicate Alerts: {len(duplicates)} groups")
# Overall recommendations
recommendations = analysis_results['overall_recommendations']
print(f"\nTop Recommendations:")
for i, rec in enumerate(recommendations[:5], 1):
print(f" {i}. {rec}")
print(f"\n{'='*60}\n")
def main():
"""Main function for CLI usage."""
parser = argparse.ArgumentParser(
description='Analyze and optimize alert configurations',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Analyze alert configuration
python alert_optimizer.py --input alerts.json --analyze-only
# Generate optimized configuration
python alert_optimizer.py --input alerts.json --output optimized_alerts.json
# Generate HTML report
python alert_optimizer.py --input alerts.json --report report.html --format html
"""
)
parser.add_argument('--input', '-i', required=True,
help='Input alert configuration JSON file')
parser.add_argument('--output', '-o',
help='Output optimized configuration JSON file')
parser.add_argument('--report', '-r',
help='Generate analysis report file')
parser.add_argument('--format', choices=['json', 'html'], default='json',
help='Report format (json or html)')
parser.add_argument('--analyze-only', action='store_true',
help='Only perform analysis, do not generate optimized config')
args = parser.parse_args()
optimizer = AlertOptimizer()
try:
# Load alert configuration
alert_config = optimizer.load_alert_config(args.input)
# Perform analysis
analysis_results = optimizer.analyze_configuration(alert_config)
# Generate optimized configuration if requested
if not args.analyze_only:
optimized_config = optimizer.generate_optimized_config(
alert_config.get('alerts', alert_config.get('rules', [])),
analysis_results
)
output_file = args.output or 'optimized_alerts.json'
optimizer.export_analysis(optimized_config, output_file, 'json')
print(f"Optimized configuration saved to: {output_file}")
# Generate report if requested
if args.report:
optimizer.export_analysis(analysis_results, args.report, args.format)
print(f"Analysis report saved to: {args.report}")
# Always show summary
optimizer.print_summary(analysis_results)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
FILE:scripts/dashboard_generator.py
#!/usr/bin/env python3
"""
Dashboard Generator - Generate comprehensive dashboard specifications
This script generates dashboard specifications based on service/system descriptions:
- Panel layout optimized for different screen sizes and roles
- Metric queries (Prometheus-style) for comprehensive monitoring
- Visualization types appropriate for different metric types
- Drill-down paths for effective troubleshooting workflows
- Golden signals coverage (latency, traffic, errors, saturation)
- RED/USE method implementation
- Business metrics integration
Usage:
python dashboard_generator.py --input service_definition.json --output dashboard_spec.json
python dashboard_generator.py --service-type api --name "Payment Service" --output payment_dashboard.json
"""
import json
import argparse
import sys
import math
from typing import Dict, List, Any, Tuple
from datetime import datetime, timedelta
class DashboardGenerator:
"""Generate comprehensive dashboard specifications."""
# Dashboard layout templates by role
ROLE_LAYOUTS = {
'sre': {
'primary_focus': ['availability', 'latency', 'errors', 'resource_utilization'],
'secondary_focus': ['throughput', 'capacity', 'dependencies'],
'time_ranges': ['1h', '6h', '1d', '7d'],
'default_refresh': '30s'
},
'developer': {
'primary_focus': ['latency', 'errors', 'throughput', 'business_metrics'],
'secondary_focus': ['resource_utilization', 'dependencies'],
'time_ranges': ['15m', '1h', '6h', '1d'],
'default_refresh': '1m'
},
'executive': {
'primary_focus': ['availability', 'business_metrics', 'user_experience'],
'secondary_focus': ['cost', 'capacity_trends'],
'time_ranges': ['1d', '7d', '30d'],
'default_refresh': '5m'
},
'ops': {
'primary_focus': ['resource_utilization', 'capacity', 'alerts', 'deployments'],
'secondary_focus': ['throughput', 'latency'],
'time_ranges': ['5m', '30m', '2h', '1d'],
'default_refresh': '15s'
}
}
# Service type specific metric configurations
SERVICE_METRICS = {
'api': {
'golden_signals': ['latency', 'traffic', 'errors', 'saturation'],
'key_metrics': [
'http_requests_total',
'http_request_duration_seconds',
'http_request_size_bytes',
'http_response_size_bytes'
],
'resource_metrics': ['cpu_usage', 'memory_usage', 'goroutines']
},
'web': {
'golden_signals': ['latency', 'traffic', 'errors', 'saturation'],
'key_metrics': [
'http_requests_total',
'http_request_duration_seconds',
'page_load_time',
'user_sessions'
],
'resource_metrics': ['cpu_usage', 'memory_usage', 'connections']
},
'database': {
'golden_signals': ['latency', 'traffic', 'errors', 'saturation'],
'key_metrics': [
'db_connections_active',
'db_query_duration_seconds',
'db_queries_total',
'db_slow_queries_total'
],
'resource_metrics': ['cpu_usage', 'memory_usage', 'disk_io', 'connections']
},
'queue': {
'golden_signals': ['latency', 'traffic', 'errors', 'saturation'],
'key_metrics': [
'queue_depth',
'message_processing_duration',
'messages_published_total',
'messages_consumed_total'
],
'resource_metrics': ['cpu_usage', 'memory_usage', 'disk_usage']
}
}
# Visualization type recommendations
VISUALIZATION_TYPES = {
'latency': 'line_chart',
'throughput': 'line_chart',
'error_rate': 'line_chart',
'success_rate': 'stat',
'resource_utilization': 'gauge',
'queue_depth': 'bar_chart',
'status': 'stat',
'distribution': 'heatmap',
'alerts': 'table',
'logs': 'logs_panel'
}
def __init__(self):
"""Initialize the Dashboard Generator."""
self.service_config = {}
self.dashboard_spec = {}
def load_service_definition(self, file_path: str) -> Dict[str, Any]:
"""Load service definition from JSON file."""
try:
with open(file_path, 'r') as f:
return json.load(f)
except FileNotFoundError:
raise ValueError(f"Service definition file not found: {file_path}")
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in service definition: {e}")
def create_service_definition(self, service_type: str, name: str,
criticality: str = 'medium') -> Dict[str, Any]:
"""Create a service definition from parameters."""
return {
'name': name,
'type': service_type,
'criticality': criticality,
'description': f'{name} - A {criticality} criticality {service_type} service',
'team': 'platform',
'environment': 'production',
'dependencies': [],
'tags': []
}
def generate_dashboard_specification(self, service_def: Dict[str, Any],
target_role: str = 'sre') -> Dict[str, Any]:
"""Generate comprehensive dashboard specification."""
service_name = service_def.get('name', 'Service')
service_type = service_def.get('type', 'api')
# Get role-specific configuration
role_config = self.ROLE_LAYOUTS.get(target_role, self.ROLE_LAYOUTS['sre'])
dashboard_spec = {
'metadata': {
'title': f"{service_name} - {target_role.upper()} Dashboard",
'service': service_def,
'target_role': target_role,
'generated_at': datetime.utcnow().isoformat() + 'Z',
'version': '1.0'
},
'configuration': {
'time_ranges': role_config['time_ranges'],
'default_time_range': role_config['time_ranges'][1], # Second option as default
'refresh_interval': role_config['default_refresh'],
'timezone': 'UTC',
'theme': 'dark'
},
'layout': self._generate_dashboard_layout(service_def, role_config),
'panels': self._generate_panels(service_def, role_config),
'variables': self._generate_template_variables(service_def),
'alerts_integration': self._generate_alerts_integration(service_def),
'drill_down_paths': self._generate_drill_down_paths(service_def)
}
return dashboard_spec
def _generate_dashboard_layout(self, service_def: Dict[str, Any],
role_config: Dict[str, Any]) -> Dict[str, Any]:
"""Generate dashboard layout configuration."""
return {
'grid_settings': {
'width': 24, # Grafana-style 24-column grid
'height_unit': 'px',
'cell_height': 30
},
'sections': [
{
'title': 'Service Overview',
'collapsed': False,
'y_position': 0,
'panels': ['service_status', 'slo_summary', 'error_budget']
},
{
'title': 'Golden Signals',
'collapsed': False,
'y_position': 8,
'panels': ['latency', 'traffic', 'errors', 'saturation']
},
{
'title': 'Resource Utilization',
'collapsed': False,
'y_position': 16,
'panels': ['cpu_usage', 'memory_usage', 'network_io', 'disk_io']
},
{
'title': 'Dependencies & Downstream',
'collapsed': True,
'y_position': 24,
'panels': ['dependency_status', 'downstream_latency', 'circuit_breakers']
}
]
}
def _generate_panels(self, service_def: Dict[str, Any],
role_config: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Generate dashboard panels based on service and role."""
service_name = service_def.get('name', 'service')
service_type = service_def.get('type', 'api')
panels = []
# Service Overview Panels
panels.extend(self._create_overview_panels(service_def))
# Golden Signals Panels
panels.extend(self._create_golden_signals_panels(service_def))
# Resource Utilization Panels
panels.extend(self._create_resource_panels(service_def))
# Service-specific panels
if service_type == 'api':
panels.extend(self._create_api_specific_panels(service_def))
elif service_type == 'database':
panels.extend(self._create_database_specific_panels(service_def))
elif service_type == 'queue':
panels.extend(self._create_queue_specific_panels(service_def))
# Role-specific additional panels
if 'business_metrics' in role_config['primary_focus']:
panels.extend(self._create_business_metrics_panels(service_def))
if 'capacity' in role_config['primary_focus']:
panels.extend(self._create_capacity_panels(service_def))
return panels
def _create_overview_panels(self, service_def: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Create service overview panels."""
service_name = service_def.get('name', 'service')
return [
{
'id': 'service_status',
'title': 'Service Status',
'type': 'stat',
'grid_pos': {'x': 0, 'y': 0, 'w': 6, 'h': 4},
'targets': [
{
'expr': f'up{{service="{service_name}"}}',
'legendFormat': 'Status'
}
],
'field_config': {
'overrides': [
{
'matcher': {'id': 'byName', 'options': 'Status'},
'properties': [
{'id': 'color', 'value': {'mode': 'thresholds'}},
{'id': 'thresholds', 'value': {
'steps': [
{'color': 'red', 'value': 0},
{'color': 'green', 'value': 1}
]
}},
{'id': 'mappings', 'value': [
{'options': {'0': {'text': 'DOWN'}}, 'type': 'value'},
{'options': {'1': {'text': 'UP'}}, 'type': 'value'}
]}
]
}
]
},
'options': {
'orientation': 'horizontal',
'textMode': 'value_and_name'
}
},
{
'id': 'slo_summary',
'title': 'SLO Achievement (30d)',
'type': 'stat',
'grid_pos': {'x': 6, 'y': 0, 'w': 9, 'h': 4},
'targets': [
{
'expr': f'(1 - (increase(http_requests_total{{service="{service_name}",code=~"5.."}}[30d]) / increase(http_requests_total{{service="{service_name}"}}[30d]))) * 100',
'legendFormat': 'Availability'
},
{
'expr': f'histogram_quantile(0.95, increase(http_request_duration_seconds_bucket{{service="{service_name}"}}[30d])) * 1000',
'legendFormat': 'P95 Latency (ms)'
}
],
'field_config': {
'defaults': {
'color': {'mode': 'thresholds'},
'thresholds': {
'steps': [
{'color': 'red', 'value': 0},
{'color': 'yellow', 'value': 99.0},
{'color': 'green', 'value': 99.9}
]
}
}
},
'options': {
'orientation': 'horizontal',
'textMode': 'value_and_name'
}
},
{
'id': 'error_budget',
'title': 'Error Budget Remaining',
'type': 'gauge',
'grid_pos': {'x': 15, 'y': 0, 'w': 9, 'h': 4},
'targets': [
{
'expr': f'(1 - (increase(http_requests_total{{service="{service_name}",code=~"5.."}}[30d]) / increase(http_requests_total{{service="{service_name}"}}[30d])) - 0.999) / 0.001 * 100',
'legendFormat': 'Error Budget %'
}
],
'field_config': {
'defaults': {
'color': {'mode': 'thresholds'},
'min': 0,
'max': 100,
'thresholds': {
'steps': [
{'color': 'red', 'value': 0},
{'color': 'yellow', 'value': 25},
{'color': 'green', 'value': 50}
]
},
'unit': 'percent'
}
},
'options': {
'showThresholdLabels': True,
'showThresholdMarkers': True
}
}
]
def _create_golden_signals_panels(self, service_def: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Create golden signals monitoring panels."""
service_name = service_def.get('name', 'service')
return [
{
'id': 'latency',
'title': 'Request Latency',
'type': 'timeseries',
'grid_pos': {'x': 0, 'y': 8, 'w': 12, 'h': 6},
'targets': [
{
'expr': f'histogram_quantile(0.50, rate(http_request_duration_seconds_bucket{{service="{service_name}"}}[5m])) * 1000',
'legendFormat': 'P50 Latency'
},
{
'expr': f'histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{{service="{service_name}"}}[5m])) * 1000',
'legendFormat': 'P95 Latency'
},
{
'expr': f'histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{{service="{service_name}"}}[5m])) * 1000',
'legendFormat': 'P99 Latency'
}
],
'field_config': {
'defaults': {
'color': {'mode': 'palette-classic'},
'unit': 'ms',
'custom': {
'drawStyle': 'line',
'lineInterpolation': 'linear',
'lineWidth': 1,
'fillOpacity': 10
}
}
},
'options': {
'tooltip': {'mode': 'multi', 'sort': 'desc'},
'legend': {'displayMode': 'table', 'placement': 'bottom'}
}
},
{
'id': 'traffic',
'title': 'Request Rate',
'type': 'timeseries',
'grid_pos': {'x': 12, 'y': 8, 'w': 12, 'h': 6},
'targets': [
{
'expr': f'sum(rate(http_requests_total{{service="{service_name}"}}[5m]))',
'legendFormat': 'Total RPS'
},
{
'expr': f'sum(rate(http_requests_total{{service="{service_name}",code=~"2.."}}[5m]))',
'legendFormat': '2xx RPS'
},
{
'expr': f'sum(rate(http_requests_total{{service="{service_name}",code=~"4.."}}[5m]))',
'legendFormat': '4xx RPS'
},
{
'expr': f'sum(rate(http_requests_total{{service="{service_name}",code=~"5.."}}[5m]))',
'legendFormat': '5xx RPS'
}
],
'field_config': {
'defaults': {
'color': {'mode': 'palette-classic'},
'unit': 'reqps',
'custom': {
'drawStyle': 'line',
'lineInterpolation': 'linear',
'lineWidth': 1,
'fillOpacity': 0
}
}
},
'options': {
'tooltip': {'mode': 'multi', 'sort': 'desc'},
'legend': {'displayMode': 'table', 'placement': 'bottom'}
}
},
{
'id': 'errors',
'title': 'Error Rate',
'type': 'timeseries',
'grid_pos': {'x': 0, 'y': 14, 'w': 12, 'h': 6},
'targets': [
{
'expr': f'sum(rate(http_requests_total{{service="{service_name}",code=~"5.."}}[5m])) / sum(rate(http_requests_total{{service="{service_name}"}}[5m])) * 100',
'legendFormat': '5xx Error Rate'
},
{
'expr': f'sum(rate(http_requests_total{{service="{service_name}",code=~"4.."}}[5m])) / sum(rate(http_requests_total{{service="{service_name}"}}[5m])) * 100',
'legendFormat': '4xx Error Rate'
}
],
'field_config': {
'defaults': {
'color': {'mode': 'palette-classic'},
'unit': 'percent',
'custom': {
'drawStyle': 'line',
'lineInterpolation': 'linear',
'lineWidth': 2,
'fillOpacity': 20
}
},
'overrides': [
{
'matcher': {'id': 'byName', 'options': '5xx Error Rate'},
'properties': [{'id': 'color', 'value': {'fixedColor': 'red'}}]
}
]
},
'options': {
'tooltip': {'mode': 'multi', 'sort': 'desc'},
'legend': {'displayMode': 'table', 'placement': 'bottom'}
}
},
{
'id': 'saturation',
'title': 'Saturation Metrics',
'type': 'timeseries',
'grid_pos': {'x': 12, 'y': 14, 'w': 12, 'h': 6},
'targets': [
{
'expr': f'rate(process_cpu_seconds_total{{service="{service_name}"}}[5m]) * 100',
'legendFormat': 'CPU Usage %'
},
{
'expr': f'process_resident_memory_bytes{{service="{service_name}"}} / process_virtual_memory_max_bytes{{service="{service_name}"}} * 100',
'legendFormat': 'Memory Usage %'
}
],
'field_config': {
'defaults': {
'color': {'mode': 'palette-classic'},
'unit': 'percent',
'max': 100,
'custom': {
'drawStyle': 'line',
'lineInterpolation': 'linear',
'lineWidth': 1,
'fillOpacity': 10
}
}
},
'options': {
'tooltip': {'mode': 'multi', 'sort': 'desc'},
'legend': {'displayMode': 'table', 'placement': 'bottom'}
}
}
]
def _create_resource_panels(self, service_def: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Create resource utilization panels."""
service_name = service_def.get('name', 'service')
return [
{
'id': 'cpu_usage',
'title': 'CPU Usage',
'type': 'gauge',
'grid_pos': {'x': 0, 'y': 20, 'w': 6, 'h': 4},
'targets': [
{
'expr': f'rate(process_cpu_seconds_total{{service="{service_name}"}}[5m]) * 100',
'legendFormat': 'CPU %'
}
],
'field_config': {
'defaults': {
'color': {'mode': 'thresholds'},
'unit': 'percent',
'min': 0,
'max': 100,
'thresholds': {
'steps': [
{'color': 'green', 'value': 0},
{'color': 'yellow', 'value': 70},
{'color': 'red', 'value': 90}
]
}
}
},
'options': {
'showThresholdLabels': True,
'showThresholdMarkers': True
}
},
{
'id': 'memory_usage',
'title': 'Memory Usage',
'type': 'gauge',
'grid_pos': {'x': 6, 'y': 20, 'w': 6, 'h': 4},
'targets': [
{
'expr': f'process_resident_memory_bytes{{service="{service_name}"}} / 1024 / 1024',
'legendFormat': 'Memory MB'
}
],
'field_config': {
'defaults': {
'color': {'mode': 'thresholds'},
'unit': 'decbytes',
'thresholds': {
'steps': [
{'color': 'green', 'value': 0},
{'color': 'yellow', 'value': 512000000}, # 512MB
{'color': 'red', 'value': 1024000000} # 1GB
]
}
}
}
},
{
'id': 'network_io',
'title': 'Network I/O',
'type': 'timeseries',
'grid_pos': {'x': 12, 'y': 20, 'w': 6, 'h': 4},
'targets': [
{
'expr': f'rate(process_network_receive_bytes_total{{service="{service_name}"}}[5m])',
'legendFormat': 'RX Bytes/s'
},
{
'expr': f'rate(process_network_transmit_bytes_total{{service="{service_name}"}}[5m])',
'legendFormat': 'TX Bytes/s'
}
],
'field_config': {
'defaults': {
'color': {'mode': 'palette-classic'},
'unit': 'binBps'
}
}
},
{
'id': 'disk_io',
'title': 'Disk I/O',
'type': 'timeseries',
'grid_pos': {'x': 18, 'y': 20, 'w': 6, 'h': 4},
'targets': [
{
'expr': f'rate(process_disk_read_bytes_total{{service="{service_name}"}}[5m])',
'legendFormat': 'Read Bytes/s'
},
{
'expr': f'rate(process_disk_write_bytes_total{{service="{service_name}"}}[5m])',
'legendFormat': 'Write Bytes/s'
}
],
'field_config': {
'defaults': {
'color': {'mode': 'palette-classic'},
'unit': 'binBps'
}
}
}
]
def _create_api_specific_panels(self, service_def: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Create API-specific panels."""
service_name = service_def.get('name', 'service')
return [
{
'id': 'endpoint_latency',
'title': 'Top Slowest Endpoints',
'type': 'table',
'grid_pos': {'x': 0, 'y': 24, 'w': 12, 'h': 6},
'targets': [
{
'expr': f'topk(10, histogram_quantile(0.95, sum by (handler) (rate(http_request_duration_seconds_bucket{{service="{service_name}"}}[5m])))) * 1000',
'legendFormat': '{{handler}}',
'format': 'table',
'instant': True
}
],
'transformations': [
{
'id': 'organize',
'options': {
'excludeByName': {'Time': True},
'renameByName': {'Value': 'P95 Latency (ms)'}
}
}
],
'field_config': {
'overrides': [
{
'matcher': {'id': 'byName', 'options': 'P95 Latency (ms)'},
'properties': [
{'id': 'color', 'value': {'mode': 'thresholds'}},
{'id': 'thresholds', 'value': {
'steps': [
{'color': 'green', 'value': 0},
{'color': 'yellow', 'value': 100},
{'color': 'red', 'value': 500}
]
}}
]
}
]
}
},
{
'id': 'request_size_distribution',
'title': 'Request Size Distribution',
'type': 'heatmap',
'grid_pos': {'x': 12, 'y': 24, 'w': 12, 'h': 6},
'targets': [
{
'expr': f'sum by (le) (rate(http_request_size_bytes_bucket{{service="{service_name}"}}[5m]))',
'legendFormat': '{{le}}'
}
],
'options': {
'calculate': True,
'yAxis': {'unit': 'bytes'},
'color': {'scheme': 'Spectral'}
}
}
]
def _create_database_specific_panels(self, service_def: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Create database-specific panels."""
service_name = service_def.get('name', 'service')
return [
{
'id': 'db_connections',
'title': 'Database Connections',
'type': 'timeseries',
'grid_pos': {'x': 0, 'y': 24, 'w': 8, 'h': 6},
'targets': [
{
'expr': f'db_connections_active{{service="{service_name}"}}',
'legendFormat': 'Active Connections'
},
{
'expr': f'db_connections_idle{{service="{service_name}"}}',
'legendFormat': 'Idle Connections'
},
{
'expr': f'db_connections_max{{service="{service_name}"}}',
'legendFormat': 'Max Connections'
}
]
},
{
'id': 'query_performance',
'title': 'Query Performance',
'type': 'timeseries',
'grid_pos': {'x': 8, 'y': 24, 'w': 8, 'h': 6},
'targets': [
{
'expr': f'rate(db_queries_total{{service="{service_name}"}}[5m])',
'legendFormat': 'Queries/sec'
},
{
'expr': f'rate(db_slow_queries_total{{service="{service_name}"}}[5m])',
'legendFormat': 'Slow Queries/sec'
}
]
},
{
'id': 'db_locks',
'title': 'Database Locks',
'type': 'stat',
'grid_pos': {'x': 16, 'y': 24, 'w': 8, 'h': 6},
'targets': [
{
'expr': f'db_locks_waiting{{service="{service_name}"}}',
'legendFormat': 'Waiting Locks'
}
],
'field_config': {
'defaults': {
'color': {'mode': 'thresholds'},
'thresholds': {
'steps': [
{'color': 'green', 'value': 0},
{'color': 'yellow', 'value': 1},
{'color': 'red', 'value': 5}
]
}
}
}
}
]
def _create_queue_specific_panels(self, service_def: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Create queue-specific panels."""
service_name = service_def.get('name', 'service')
return [
{
'id': 'queue_depth',
'title': 'Queue Depth',
'type': 'timeseries',
'grid_pos': {'x': 0, 'y': 24, 'w': 12, 'h': 6},
'targets': [
{
'expr': f'queue_depth{{service="{service_name}"}}',
'legendFormat': 'Messages in Queue'
}
]
},
{
'id': 'message_throughput',
'title': 'Message Throughput',
'type': 'timeseries',
'grid_pos': {'x': 12, 'y': 24, 'w': 12, 'h': 6},
'targets': [
{
'expr': f'rate(messages_published_total{{service="{service_name}"}}[5m])',
'legendFormat': 'Published/sec'
},
{
'expr': f'rate(messages_consumed_total{{service="{service_name}"}}[5m])',
'legendFormat': 'Consumed/sec'
}
]
}
]
def _create_business_metrics_panels(self, service_def: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Create business metrics panels."""
service_name = service_def.get('name', 'service')
return [
{
'id': 'business_kpis',
'title': 'Business KPIs',
'type': 'stat',
'grid_pos': {'x': 0, 'y': 30, 'w': 24, 'h': 4},
'targets': [
{
'expr': f'rate(business_transactions_total{{service="{service_name}"}}[1h])',
'legendFormat': 'Transactions/hour'
},
{
'expr': f'avg(business_transaction_value{{service="{service_name}"}}) * rate(business_transactions_total{{service="{service_name}"}}[1h])',
'legendFormat': 'Revenue/hour'
},
{
'expr': f'rate(user_registrations_total{{service="{service_name}"}}[1h])',
'legendFormat': 'New Users/hour'
}
],
'field_config': {
'defaults': {
'color': {'mode': 'palette-classic'},
'custom': {
'displayMode': 'basic'
}
}
},
'options': {
'orientation': 'horizontal',
'textMode': 'value_and_name'
}
}
]
def _create_capacity_panels(self, service_def: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Create capacity planning panels."""
service_name = service_def.get('name', 'service')
return [
{
'id': 'capacity_trends',
'title': 'Capacity Trends (7d)',
'type': 'timeseries',
'grid_pos': {'x': 0, 'y': 34, 'w': 24, 'h': 6},
'targets': [
{
'expr': f'predict_linear(avg_over_time(rate(http_requests_total{{service="{service_name}"}}[5m])[7d:1h]), 7*24*3600)',
'legendFormat': 'Predicted Traffic (7d)'
},
{
'expr': f'predict_linear(avg_over_time(process_resident_memory_bytes{{service="{service_name}"}}[7d:1h]), 7*24*3600)',
'legendFormat': 'Predicted Memory Usage (7d)'
}
],
'field_config': {
'defaults': {
'color': {'mode': 'palette-classic'},
'custom': {
'drawStyle': 'line',
'lineStyle': {'dash': [10, 10]}
}
}
}
}
]
def _generate_template_variables(self, service_def: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Generate template variables for dynamic dashboard filtering."""
service_name = service_def.get('name', 'service')
return [
{
'name': 'environment',
'type': 'query',
'query': 'label_values(environment)',
'current': {'text': 'production', 'value': 'production'},
'includeAll': False,
'multi': False,
'refresh': 'on_dashboard_load'
},
{
'name': 'instance',
'type': 'query',
'query': f'label_values(up{{service="{service_name}"}}, instance)',
'current': {'text': 'All', 'value': '$__all'},
'includeAll': True,
'multi': True,
'refresh': 'on_time_range_change'
},
{
'name': 'handler',
'type': 'query',
'query': f'label_values(http_requests_total{{service="{service_name}"}}, handler)',
'current': {'text': 'All', 'value': '$__all'},
'includeAll': True,
'multi': True,
'refresh': 'on_time_range_change'
}
]
def _generate_alerts_integration(self, service_def: Dict[str, Any]) -> Dict[str, Any]:
"""Generate alerts integration configuration."""
service_name = service_def.get('name', 'service')
return {
'alert_annotations': True,
'alert_rules_query': f'ALERTS{{service="{service_name}"}}',
'alert_panels': [
{
'title': 'Active Alerts',
'type': 'table',
'query': f'ALERTS{{service="{service_name}",alertstate="firing"}}',
'columns': ['alertname', 'severity', 'instance', 'description']
}
]
}
def _generate_drill_down_paths(self, service_def: Dict[str, Any]) -> Dict[str, Any]:
"""Generate drill-down navigation paths."""
service_name = service_def.get('name', 'service')
return {
'service_overview': {
'from': 'service_status',
'to': 'detailed_health_dashboard',
'url': f'/d/service-health/{service_name}-health',
'params': ['var-service', 'var-environment']
},
'error_investigation': {
'from': 'errors',
'to': 'error_details_dashboard',
'url': f'/d/errors/{service_name}-errors',
'params': ['var-service', 'var-time_range']
},
'latency_analysis': {
'from': 'latency',
'to': 'trace_analysis_dashboard',
'url': f'/d/traces/{service_name}-traces',
'params': ['var-service', 'var-handler']
},
'capacity_planning': {
'from': 'saturation',
'to': 'capacity_dashboard',
'url': f'/d/capacity/{service_name}-capacity',
'params': ['var-service', 'var-time_range']
}
}
def generate_grafana_json(self, dashboard_spec: Dict[str, Any]) -> Dict[str, Any]:
"""Convert dashboard specification to Grafana JSON format."""
metadata = dashboard_spec['metadata']
config = dashboard_spec['configuration']
grafana_json = {
'dashboard': {
'id': None,
'title': metadata['title'],
'tags': [metadata['service']['type'], metadata['target_role'], 'generated'],
'timezone': config['timezone'],
'refresh': config['refresh_interval'],
'time': {
'from': 'now-1h',
'to': 'now'
},
'templating': {
'list': dashboard_spec['variables']
},
'panels': self._convert_panels_to_grafana_format(dashboard_spec['panels']),
'version': 1,
'schemaVersion': 30
},
'overwrite': True
}
return grafana_json
def _convert_panels_to_grafana_format(self, panels: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Convert panel specifications to Grafana format."""
grafana_panels = []
for panel in panels:
grafana_panel = {
'id': hash(panel['id']) % 1000, # Generate numeric ID
'title': panel['title'],
'type': panel['type'],
'gridPos': panel['grid_pos'],
'targets': panel['targets'],
'fieldConfig': panel.get('field_config', {}),
'options': panel.get('options', {}),
'transformations': panel.get('transformations', [])
}
grafana_panels.append(grafana_panel)
return grafana_panels
def generate_documentation(self, dashboard_spec: Dict[str, Any]) -> str:
"""Generate documentation for the dashboard."""
metadata = dashboard_spec['metadata']
service = metadata['service']
doc_content = f"""# {metadata['title']} Documentation
## Overview
This dashboard provides comprehensive monitoring for {service['name']}, a {service['type']} service with {service['criticality']} criticality.
**Target Audience:** {metadata['target_role'].upper()} teams
**Generated:** {metadata['generated_at']}
## Dashboard Sections
### Service Overview
- **Service Status**: Real-time availability status
- **SLO Achievement**: 30-day SLO compliance metrics
- **Error Budget**: Remaining error budget visualization
### Golden Signals Monitoring
- **Latency**: P50, P95, P99 response times
- **Traffic**: Request rate by status code
- **Errors**: Error rates for 4xx and 5xx responses
- **Saturation**: CPU and memory utilization
### Resource Utilization
- **CPU Usage**: Process CPU consumption
- **Memory Usage**: Memory utilization tracking
- **Network I/O**: Network throughput metrics
- **Disk I/O**: Disk read/write operations
## Key Metrics
### SLIs Tracked
"""
# Add service-type specific metrics
service_type = service.get('type', 'api')
if service_type in self.SERVICE_METRICS:
metrics = self.SERVICE_METRICS[service_type]['key_metrics']
for metric in metrics:
doc_content += f"- `{metric}`: Core service metric\n"
doc_content += f"""
## Alert Integration
- Active alerts are displayed in context with relevant panels
- Alert annotations show on time series charts
- Click-through to alert management system available
## Drill-Down Paths
"""
drill_downs = dashboard_spec.get('drill_down_paths', {})
for path_name, path_config in drill_downs.items():
doc_content += f"- **{path_name}**: From {path_config['from']} → {path_config['to']}\n"
doc_content += f"""
## Usage Guidelines
### Time Ranges
Use appropriate time ranges for different investigation types:
- **Real-time monitoring**: 15m - 1h
- **Recent incident investigation**: 1h - 6h
- **Trend analysis**: 1d - 7d
- **Capacity planning**: 7d - 30d
### Variables
- **environment**: Filter by deployment environment
- **instance**: Focus on specific service instances
- **handler**: Filter by API endpoint or handler
### Performance Optimization
- Use longer time ranges for capacity planning
- Refresh intervals are optimized per role:
- SRE: 30s for operational awareness
- Developer: 1m for troubleshooting
- Executive: 5m for high-level monitoring
## Maintenance
- Dashboard panels automatically adapt to service changes
- Template variables refresh based on actual metric labels
- Review and update business metrics quarterly
"""
return doc_content
def export_specification(self, dashboard_spec: Dict[str, Any], output_file: str,
format_type: str = 'json'):
"""Export dashboard specification."""
if format_type.lower() == 'json':
with open(output_file, 'w') as f:
json.dump(dashboard_spec, f, indent=2)
elif format_type.lower() == 'grafana':
grafana_json = self.generate_grafana_json(dashboard_spec)
with open(output_file, 'w') as f:
json.dump(grafana_json, f, indent=2)
else:
raise ValueError(f"Unsupported format: {format_type}")
def print_summary(self, dashboard_spec: Dict[str, Any]):
"""Print human-readable summary of dashboard specification."""
metadata = dashboard_spec['metadata']
service = metadata['service']
config = dashboard_spec['configuration']
panels = dashboard_spec['panels']
print(f"\n{'='*60}")
print(f"DASHBOARD SPECIFICATION SUMMARY")
print(f"{'='*60}")
print(f"\nDashboard Details:")
print(f" Title: {metadata['title']}")
print(f" Target Role: {metadata['target_role'].upper()}")
print(f" Service: {service['name']} ({service['type']})")
print(f" Criticality: {service['criticality']}")
print(f" Generated: {metadata['generated_at']}")
print(f"\nConfiguration:")
print(f" Default Time Range: {config['default_time_range']}")
print(f" Refresh Interval: {config['refresh_interval']}")
print(f" Available Time Ranges: {', '.join(config['time_ranges'])}")
print(f"\nPanels ({len(panels)}):")
panel_types = {}
for panel in panels:
panel_type = panel['type']
panel_types[panel_type] = panel_types.get(panel_type, 0) + 1
for panel_type, count in panel_types.items():
print(f" {panel_type}: {count}")
variables = dashboard_spec.get('variables', [])
print(f"\nTemplate Variables ({len(variables)}):")
for var in variables:
print(f" {var['name']} ({var['type']})")
drill_downs = dashboard_spec.get('drill_down_paths', {})
print(f"\nDrill-down Paths: {len(drill_downs)}")
print(f"\nKey Features:")
print(f" • Golden Signals monitoring")
print(f" • Resource utilization tracking")
print(f" • Alert integration")
print(f" • Role-optimized layout")
print(f" • Service-type specific panels")
print(f"\n{'='*60}\n")
def main():
"""Main function for CLI usage."""
parser = argparse.ArgumentParser(
description='Generate comprehensive dashboard specifications',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Generate from service definition file
python dashboard_generator.py --input service.json --output dashboard.json
# Generate from command line parameters
python dashboard_generator.py --service-type api --name "Payment Service" --output payment_dashboard.json
# Generate Grafana-compatible JSON
python dashboard_generator.py --input service.json --output dashboard.json --format grafana
# Generate with specific role focus
python dashboard_generator.py --service-type web --name "Frontend" --role developer --output frontend_dev.json
"""
)
parser.add_argument('--input', '-i',
help='Input service definition JSON file')
parser.add_argument('--output', '-o',
help='Output dashboard specification file')
parser.add_argument('--service-type',
choices=['api', 'web', 'database', 'queue', 'batch', 'ml'],
help='Service type')
parser.add_argument('--name',
help='Service name')
parser.add_argument('--criticality',
choices=['critical', 'high', 'medium', 'low'],
default='medium',
help='Service criticality level')
parser.add_argument('--role',
choices=['sre', 'developer', 'executive', 'ops'],
default='sre',
help='Target role for dashboard optimization')
parser.add_argument('--format',
choices=['json', 'grafana'],
default='json',
help='Output format (json specification or grafana compatible)')
parser.add_argument('--doc-output',
help='Generate documentation file')
parser.add_argument('--summary-only', action='store_true',
help='Only display summary, do not save files')
args = parser.parse_args()
if not args.input and not (args.service_type and args.name):
parser.error("Must provide either --input file or --service-type and --name")
generator = DashboardGenerator()
try:
# Load or create service definition
if args.input:
service_def = generator.load_service_definition(args.input)
else:
service_def = generator.create_service_definition(
args.service_type, args.name, args.criticality
)
# Generate dashboard specification
dashboard_spec = generator.generate_dashboard_specification(service_def, args.role)
# Output results
if not args.summary_only:
output_file = args.output or f"{service_def['name'].replace(' ', '_').lower()}_dashboard.json"
generator.export_specification(dashboard_spec, output_file, args.format)
print(f"Dashboard specification saved to: {output_file}")
# Generate documentation if requested
if args.doc_output:
documentation = generator.generate_documentation(dashboard_spec)
with open(args.doc_output, 'w') as f:
f.write(documentation)
print(f"Documentation saved to: {args.doc_output}")
# Always show summary
generator.print_summary(dashboard_spec)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
FILE:scripts/slo_designer.py
#!/usr/bin/env python3
"""
SLO Designer - Generate comprehensive SLI/SLO frameworks for services
This script analyzes service descriptions and generates complete SLO frameworks including:
- SLI definitions based on service characteristics
- SLO targets based on criticality and user impact
- Error budget calculations and policies
- Multi-window burn rate alerts
- SLA recommendations for customer-facing services
Usage:
python slo_designer.py --input service_definition.json --output slo_framework.json
python slo_designer.py --service-type api --criticality high --user-facing true
"""
import json
import argparse
import sys
import math
from typing import Dict, List, Any, Tuple
from datetime import datetime, timedelta
class SLODesigner:
"""Design and generate SLO frameworks for services."""
# SLO target recommendations based on service criticality
SLO_TARGETS = {
'critical': {
'availability': 0.9999, # 99.99% - 4.38 minutes downtime/month
'latency_p95': 100, # 95th percentile latency in ms
'latency_p99': 500, # 99th percentile latency in ms
'error_rate': 0.001 # 0.1% error rate
},
'high': {
'availability': 0.999, # 99.9% - 43.8 minutes downtime/month
'latency_p95': 200, # 95th percentile latency in ms
'latency_p99': 1000, # 99th percentile latency in ms
'error_rate': 0.005 # 0.5% error rate
},
'medium': {
'availability': 0.995, # 99.5% - 3.65 hours downtime/month
'latency_p95': 500, # 95th percentile latency in ms
'latency_p99': 2000, # 99th percentile latency in ms
'error_rate': 0.01 # 1% error rate
},
'low': {
'availability': 0.99, # 99% - 7.3 hours downtime/month
'latency_p95': 1000, # 95th percentile latency in ms
'latency_p99': 5000, # 99th percentile latency in ms
'error_rate': 0.02 # 2% error rate
}
}
# Burn rate windows for multi-window alerting
BURN_RATE_WINDOWS = [
{'short': '5m', 'long': '1h', 'burn_rate': 14.4, 'budget_consumed': '2%'},
{'short': '30m', 'long': '6h', 'burn_rate': 6, 'budget_consumed': '5%'},
{'short': '2h', 'long': '1d', 'burn_rate': 3, 'budget_consumed': '10%'},
{'short': '6h', 'long': '3d', 'burn_rate': 1, 'budget_consumed': '10%'}
]
# Service type specific SLI recommendations
SERVICE_TYPE_SLIS = {
'api': ['availability', 'latency', 'error_rate', 'throughput'],
'web': ['availability', 'latency', 'error_rate', 'page_load_time'],
'database': ['availability', 'query_latency', 'connection_success_rate', 'replication_lag'],
'queue': ['availability', 'message_processing_time', 'queue_depth', 'message_loss_rate'],
'batch': ['job_success_rate', 'job_duration', 'data_freshness', 'resource_utilization'],
'ml': ['model_accuracy', 'prediction_latency', 'training_success_rate', 'feature_freshness']
}
def __init__(self):
"""Initialize the SLO Designer."""
self.service_config = {}
self.slo_framework = {}
def load_service_definition(self, file_path: str) -> Dict[str, Any]:
"""Load service definition from JSON file."""
try:
with open(file_path, 'r') as f:
return json.load(f)
except FileNotFoundError:
raise ValueError(f"Service definition file not found: {file_path}")
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in service definition: {e}")
def create_service_definition(self, service_type: str, criticality: str,
user_facing: bool, name: str = None) -> Dict[str, Any]:
"""Create a service definition from parameters."""
return {
'name': name or f'{service_type}_service',
'type': service_type,
'criticality': criticality,
'user_facing': user_facing,
'description': f'A {criticality} criticality {service_type} service',
'dependencies': [],
'team': 'platform',
'environment': 'production'
}
def generate_slis(self, service_def: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Generate Service Level Indicators based on service characteristics."""
service_type = service_def.get('type', 'api')
base_slis = self.SERVICE_TYPE_SLIS.get(service_type, ['availability', 'latency', 'error_rate'])
slis = []
for sli_name in base_slis:
sli = self._create_sli_definition(sli_name, service_def)
if sli:
slis.append(sli)
# Add user-facing specific SLIs
if service_def.get('user_facing', False):
user_slis = self._generate_user_facing_slis(service_def)
slis.extend(user_slis)
return slis
def _create_sli_definition(self, sli_name: str, service_def: Dict[str, Any]) -> Dict[str, Any]:
"""Create detailed SLI definition."""
service_name = service_def.get('name', 'service')
sli_definitions = {
'availability': {
'name': 'Availability',
'description': 'Percentage of successful requests',
'type': 'ratio',
'good_events': f'sum(rate(http_requests_total{{service="{service_name}",code!~"5.."}}))',
'total_events': f'sum(rate(http_requests_total{{service="{service_name}"}}))',
'unit': 'percentage'
},
'latency': {
'name': 'Request Latency P95',
'description': '95th percentile of request latency',
'type': 'threshold',
'query': f'histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{{service="{service_name}"}}[5m]))',
'unit': 'seconds'
},
'error_rate': {
'name': 'Error Rate',
'description': 'Rate of 5xx errors',
'type': 'ratio',
'good_events': f'sum(rate(http_requests_total{{service="{service_name}",code!~"5.."}}))',
'total_events': f'sum(rate(http_requests_total{{service="{service_name}"}}))',
'unit': 'percentage'
},
'throughput': {
'name': 'Request Throughput',
'description': 'Requests per second',
'type': 'gauge',
'query': f'sum(rate(http_requests_total{{service="{service_name}"}}[5m]))',
'unit': 'requests/sec'
},
'page_load_time': {
'name': 'Page Load Time P95',
'description': '95th percentile of page load time',
'type': 'threshold',
'query': f'histogram_quantile(0.95, rate(page_load_duration_seconds_bucket{{service="{service_name}"}}[5m]))',
'unit': 'seconds'
},
'query_latency': {
'name': 'Database Query Latency P95',
'description': '95th percentile of database query latency',
'type': 'threshold',
'query': f'histogram_quantile(0.95, rate(db_query_duration_seconds_bucket{{service="{service_name}"}}[5m]))',
'unit': 'seconds'
},
'connection_success_rate': {
'name': 'Database Connection Success Rate',
'description': 'Percentage of successful database connections',
'type': 'ratio',
'good_events': f'sum(rate(db_connections_total{{service="{service_name}",status="success"}}[5m]))',
'total_events': f'sum(rate(db_connections_total{{service="{service_name}"}}[5m]))',
'unit': 'percentage'
}
}
return sli_definitions.get(sli_name)
def _generate_user_facing_slis(self, service_def: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Generate additional SLIs for user-facing services."""
service_name = service_def.get('name', 'service')
return [
{
'name': 'User Journey Success Rate',
'description': 'Percentage of successful complete user journeys',
'type': 'ratio',
'good_events': f'sum(rate(user_journey_total{{service="{service_name}",status="success"}}[5m]))',
'total_events': f'sum(rate(user_journey_total{{service="{service_name}"}}[5m]))',
'unit': 'percentage'
},
{
'name': 'Feature Availability',
'description': 'Percentage of time key features are available',
'type': 'ratio',
'good_events': f'sum(rate(feature_checks_total{{service="{service_name}",status="available"}}[5m]))',
'total_events': f'sum(rate(feature_checks_total{{service="{service_name}"}}[5m]))',
'unit': 'percentage'
}
]
def generate_slos(self, service_def: Dict[str, Any], slis: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Generate Service Level Objectives based on service criticality."""
criticality = service_def.get('criticality', 'medium')
targets = self.SLO_TARGETS.get(criticality, self.SLO_TARGETS['medium'])
slos = []
for sli in slis:
slo = self._create_slo_from_sli(sli, targets, service_def)
if slo:
slos.append(slo)
return slos
def _create_slo_from_sli(self, sli: Dict[str, Any], targets: Dict[str, float],
service_def: Dict[str, Any]) -> Dict[str, Any]:
"""Create SLO definition from SLI."""
sli_name = sli['name'].lower().replace(' ', '_')
# Map SLI names to target keys
target_mapping = {
'availability': 'availability',
'request_latency_p95': 'latency_p95',
'error_rate': 'error_rate',
'user_journey_success_rate': 'availability',
'feature_availability': 'availability',
'page_load_time_p95': 'latency_p95',
'database_query_latency_p95': 'latency_p95',
'database_connection_success_rate': 'availability'
}
target_key = target_mapping.get(sli_name)
if not target_key:
return None
target_value = targets.get(target_key)
if target_value is None:
return None
# Determine comparison operator and format target
if 'latency' in sli_name or 'duration' in sli_name:
operator = '<='
target_display = f"{target_value}ms" if target_value < 10 else f"{target_value/1000}s"
elif 'rate' in sli_name and 'error' in sli_name:
operator = '<='
target_display = f"{target_value * 100}%"
target_value = target_value # Keep as decimal
else:
operator = '>='
target_display = f"{target_value * 100}%"
# Calculate time windows
time_windows = ['1h', '1d', '7d', '30d']
slo = {
'name': f"{sli['name']} SLO",
'description': f"Service level objective for {sli['description'].lower()}",
'sli_name': sli['name'],
'target_value': target_value,
'target_display': target_display,
'operator': operator,
'time_windows': time_windows,
'measurement_window': '30d',
'service': service_def.get('name', 'service'),
'criticality': service_def.get('criticality', 'medium')
}
return slo
def calculate_error_budgets(self, slos: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Calculate error budgets for SLOs."""
error_budgets = []
for slo in slos:
if slo['operator'] == '>=': # Availability-type SLOs
target = slo['target_value']
error_budget_rate = 1 - target
# Calculate budget for different time windows
time_windows = {
'1h': 3600,
'1d': 86400,
'7d': 604800,
'30d': 2592000
}
budgets = {}
for window, seconds in time_windows.items():
budget_seconds = seconds * error_budget_rate
if budget_seconds < 60:
budgets[window] = f"{budget_seconds:.1f} seconds"
elif budget_seconds < 3600:
budgets[window] = f"{budget_seconds/60:.1f} minutes"
else:
budgets[window] = f"{budget_seconds/3600:.1f} hours"
error_budget = {
'slo_name': slo['name'],
'error_budget_rate': error_budget_rate,
'error_budget_percentage': f"{error_budget_rate * 100:.3f}%",
'budgets_by_window': budgets,
'burn_rate_alerts': self._generate_burn_rate_alerts(slo, error_budget_rate)
}
error_budgets.append(error_budget)
return error_budgets
def _generate_burn_rate_alerts(self, slo: Dict[str, Any], error_budget_rate: float) -> List[Dict[str, Any]]:
"""Generate multi-window burn rate alerts."""
alerts = []
service_name = slo['service']
sli_query = self._get_sli_query_for_burn_rate(slo)
for window_config in self.BURN_RATE_WINDOWS:
alert = {
'name': f"{slo['sli_name']} Burn Rate {window_config['budget_consumed']} Alert",
'description': f"Alert when {slo['sli_name']} is consuming error budget at {window_config['burn_rate']}x rate",
'severity': self._determine_alert_severity(float(window_config['budget_consumed'].rstrip('%'))),
'short_window': window_config['short'],
'long_window': window_config['long'],
'burn_rate_threshold': window_config['burn_rate'],
'budget_consumed': window_config['budget_consumed'],
'condition': f"({sli_query}_short > {window_config['burn_rate']}) and ({sli_query}_long > {window_config['burn_rate']})",
'annotations': {
'summary': f"High burn rate detected for {slo['sli_name']}",
'description': f"Error budget consumption rate is {window_config['burn_rate']}x normal, will exhaust {window_config['budget_consumed']} of monthly budget"
}
}
alerts.append(alert)
return alerts
def _get_sli_query_for_burn_rate(self, slo: Dict[str, Any]) -> str:
"""Generate SLI query fragment for burn rate calculation."""
service_name = slo['service']
sli_name = slo['sli_name'].lower().replace(' ', '_')
if 'availability' in sli_name or 'success' in sli_name:
return f"(1 - (sum(rate(http_requests_total{{service='{service_name}',code!~'5..'}})) / sum(rate(http_requests_total{{service='{service_name}'}}))))"
elif 'error' in sli_name:
return f"(sum(rate(http_requests_total{{service='{service_name}',code=~'5..'}})) / sum(rate(http_requests_total{{service='{service_name}'}})))"
else:
return f"sli_burn_rate_{sli_name}"
def _determine_alert_severity(self, budget_consumed_percent: float) -> str:
"""Determine alert severity based on budget consumption rate."""
if budget_consumed_percent <= 2:
return 'critical'
elif budget_consumed_percent <= 5:
return 'warning'
else:
return 'info'
def generate_sla_recommendations(self, service_def: Dict[str, Any],
slos: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Generate SLA recommendations for customer-facing services."""
if not service_def.get('user_facing', False):
return {
'applicable': False,
'reason': 'SLA not recommended for non-user-facing services'
}
criticality = service_def.get('criticality', 'medium')
# SLA targets should be more conservative than SLO targets
sla_buffer = 0.001 # 0.1% buffer below SLO
sla_recommendations = {
'applicable': True,
'service': service_def.get('name'),
'commitments': [],
'penalties': self._generate_penalty_structure(criticality),
'measurement_methodology': 'External synthetic monitoring from multiple geographic locations',
'exclusions': [
'Planned maintenance windows (with 72h advance notice)',
'Customer-side network or infrastructure issues',
'Force majeure events',
'Third-party service dependencies beyond our control'
]
}
for slo in slos:
if slo['operator'] == '>=' and 'availability' in slo['sli_name'].lower():
sla_target = max(0.9, slo['target_value'] - sla_buffer)
commitment = {
'metric': slo['sli_name'],
'target': sla_target,
'target_display': f"{sla_target * 100:.2f}%",
'measurement_window': 'monthly',
'measurement_method': 'Uptime monitoring with 1-minute granularity'
}
sla_recommendations['commitments'].append(commitment)
return sla_recommendations
def _generate_penalty_structure(self, criticality: str) -> List[Dict[str, Any]]:
"""Generate penalty structure based on service criticality."""
penalty_structures = {
'critical': [
{'breach_threshold': '< 99.99%', 'credit_percentage': 10},
{'breach_threshold': '< 99.9%', 'credit_percentage': 25},
{'breach_threshold': '< 99%', 'credit_percentage': 50}
],
'high': [
{'breach_threshold': '< 99.9%', 'credit_percentage': 10},
{'breach_threshold': '< 99.5%', 'credit_percentage': 25}
],
'medium': [
{'breach_threshold': '< 99.5%', 'credit_percentage': 10}
],
'low': []
}
return penalty_structures.get(criticality, [])
def generate_framework(self, service_def: Dict[str, Any]) -> Dict[str, Any]:
"""Generate complete SLO framework."""
# Generate SLIs
slis = self.generate_slis(service_def)
# Generate SLOs
slos = self.generate_slos(service_def, slis)
# Calculate error budgets
error_budgets = self.calculate_error_budgets(slos)
# Generate SLA recommendations
sla_recommendations = self.generate_sla_recommendations(service_def, slos)
# Create comprehensive framework
framework = {
'metadata': {
'service': service_def,
'generated_at': datetime.utcnow().isoformat() + 'Z',
'framework_version': '1.0'
},
'slis': slis,
'slos': slos,
'error_budgets': error_budgets,
'sla_recommendations': sla_recommendations,
'monitoring_recommendations': self._generate_monitoring_recommendations(service_def),
'implementation_guide': self._generate_implementation_guide(service_def, slis, slos)
}
return framework
def _generate_monitoring_recommendations(self, service_def: Dict[str, Any]) -> Dict[str, Any]:
"""Generate monitoring tool recommendations."""
service_type = service_def.get('type', 'api')
recommendations = {
'metrics': {
'collection': 'Prometheus with service discovery',
'retention': '90 days for raw metrics, 1 year for aggregated',
'alerting': 'Prometheus Alertmanager with multi-window burn rate alerts'
},
'logging': {
'format': 'Structured JSON logs with correlation IDs',
'aggregation': 'ELK stack or equivalent with proper indexing',
'retention': '30 days for debug logs, 90 days for error logs'
},
'tracing': {
'sampling': 'Adaptive sampling with 1% base rate',
'storage': 'Jaeger or Zipkin with 7-day retention',
'integration': 'OpenTelemetry instrumentation'
}
}
if service_type == 'web':
recommendations['synthetic_monitoring'] = {
'frequency': 'Every 1 minute from 3+ geographic locations',
'checks': 'Full user journey simulation',
'tools': 'Pingdom, DataDog Synthetics, or equivalent'
}
return recommendations
def _generate_implementation_guide(self, service_def: Dict[str, Any],
slis: List[Dict[str, Any]],
slos: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Generate implementation guide for the SLO framework."""
return {
'prerequisites': [
'Service instrumented with metrics collection (Prometheus format)',
'Structured logging with correlation IDs',
'Monitoring infrastructure (Prometheus, Grafana, Alertmanager)',
'Incident response processes and escalation policies'
],
'implementation_steps': [
{
'step': 1,
'title': 'Instrument Service',
'description': 'Add metrics collection for all defined SLIs',
'estimated_effort': '1-2 days'
},
{
'step': 2,
'title': 'Configure Recording Rules',
'description': 'Set up Prometheus recording rules for SLI calculations',
'estimated_effort': '4-8 hours'
},
{
'step': 3,
'title': 'Implement Burn Rate Alerts',
'description': 'Configure multi-window burn rate alerting rules',
'estimated_effort': '1 day'
},
{
'step': 4,
'title': 'Create SLO Dashboard',
'description': 'Build Grafana dashboard for SLO tracking and error budget monitoring',
'estimated_effort': '4-6 hours'
},
{
'step': 5,
'title': 'Test and Validate',
'description': 'Test alerting and validate SLI measurements against expectations',
'estimated_effort': '1-2 days'
},
{
'step': 6,
'title': 'Documentation and Training',
'description': 'Document runbooks and train team on SLO monitoring',
'estimated_effort': '1 day'
}
],
'validation_checklist': [
'All SLIs produce expected metric values',
'Burn rate alerts fire correctly during simulated outages',
'Error budget calculations match manual verification',
'Dashboard displays accurate SLO achievement rates',
'Alert routing reaches correct escalation paths',
'Runbooks are complete and tested'
]
}
def export_json(self, framework: Dict[str, Any], output_file: str):
"""Export framework as JSON."""
with open(output_file, 'w') as f:
json.dump(framework, f, indent=2)
def print_summary(self, framework: Dict[str, Any]):
"""Print human-readable summary of the SLO framework."""
service = framework['metadata']['service']
slis = framework['slis']
slos = framework['slos']
error_budgets = framework['error_budgets']
print(f"\n{'='*60}")
print(f"SLO FRAMEWORK SUMMARY FOR {service['name'].upper()}")
print(f"{'='*60}")
print(f"\nService Details:")
print(f" Type: {service['type']}")
print(f" Criticality: {service['criticality']}")
print(f" User Facing: {'Yes' if service.get('user_facing') else 'No'}")
print(f" Team: {service.get('team', 'Unknown')}")
print(f"\nService Level Indicators ({len(slis)}):")
for i, sli in enumerate(slis, 1):
print(f" {i}. {sli['name']}")
print(f" Description: {sli['description']}")
print(f" Type: {sli['type']}")
print()
print(f"Service Level Objectives ({len(slos)}):")
for i, slo in enumerate(slos, 1):
print(f" {i}. {slo['name']}")
print(f" Target: {slo['target_display']}")
print(f" Measurement Window: {slo['measurement_window']}")
print()
print(f"Error Budget Summary:")
for budget in error_budgets:
print(f" {budget['slo_name']}:")
print(f" Monthly Budget: {budget['error_budget_percentage']}")
print(f" Burn Rate Alerts: {len(budget['burn_rate_alerts'])}")
print()
sla = framework['sla_recommendations']
if sla['applicable']:
print(f"SLA Recommendations:")
print(f" Commitments: {len(sla['commitments'])}")
print(f" Penalty Tiers: {len(sla['penalties'])}")
else:
print(f"SLA Recommendations: {sla['reason']}")
print(f"\nImplementation Timeline: 1-2 weeks")
print(f"Framework generated at: {framework['metadata']['generated_at']}")
print(f"{'='*60}\n")
def main():
"""Main function for CLI usage."""
parser = argparse.ArgumentParser(
description='Generate comprehensive SLO frameworks for services',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Generate from service definition file
python slo_designer.py --input service.json --output framework.json
# Generate from command line parameters
python slo_designer.py --service-type api --criticality high --user-facing true --output framework.json
# Generate and display summary only
python slo_designer.py --service-type web --criticality critical --user-facing true --summary-only
"""
)
parser.add_argument('--input', '-i',
help='Input service definition JSON file')
parser.add_argument('--output', '-o',
help='Output framework JSON file')
parser.add_argument('--service-type',
choices=['api', 'web', 'database', 'queue', 'batch', 'ml'],
help='Service type')
parser.add_argument('--criticality',
choices=['critical', 'high', 'medium', 'low'],
help='Service criticality level')
parser.add_argument('--user-facing',
choices=['true', 'false'],
help='Whether service is user-facing')
parser.add_argument('--service-name',
help='Service name')
parser.add_argument('--summary-only', action='store_true',
help='Only display summary, do not save JSON')
args = parser.parse_args()
if not args.input and not (args.service_type and args.criticality and args.user_facing):
parser.error("Must provide either --input file or --service-type, --criticality, and --user-facing")
designer = SLODesigner()
try:
# Load or create service definition
if args.input:
service_def = designer.load_service_definition(args.input)
else:
user_facing = args.user_facing.lower() == 'true'
service_def = designer.create_service_definition(
args.service_type, args.criticality, user_facing, args.service_name
)
# Generate framework
framework = designer.generate_framework(service_def)
# Output results
if not args.summary_only:
output_file = args.output or f"{service_def['name']}_slo_framework.json"
designer.export_json(framework, output_file)
print(f"SLO framework saved to: {output_file}")
# Always show summary
designer.print_summary(framework)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()