Files
magnus919_agent-skills/yc-weekly-growth-compass/scripts/growth-compass.py
Magnus Hedemark d8a11c2a4b Add yc-default-alive-calculator and yc-weekly-growth-compass skills
Two research-grounded entrepreneurial tools based on Paul Graham's Y Combinator
frameworks, with companion CLI scripts and extensive reference material.

yc-default-alive-calculator:
- Paul Graham's 'Default Alive / Default Dead' framework as a deterministic CLI
- Month-by-month financial projection engine with growth decay modeling
- Burn multiple analysis, lever identification, and actionable verdict
- Zero external dependencies (Python 3.9+ stdlib only)
- 2 reference docs (framework deep-dive, fundraising context)

yc-weekly-growth-compass:
- Paul Graham's 'Startup = Growth' framework as an operational weekly tool
- Single-period and time-series growth rate computation
- YC benchmark classification (1% concerning -> 10%+ outstanding)
- Compound growth projections, doubling time, and decision compass
- Zero external dependencies (Python 3.9+ stdlib only)
- 2 reference docs (framework essay breakdown, compound growth table)

Both skills follow the Agent Skills open format (agentskills.io spec v1.0).
2026-06-13 14:53:50 -04:00

492 lines
20 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Weekly Growth Compass
Paul Graham's "Startup = Growth" framework as a CLI tool. Computes weekly
growth rates, benchmarks against YC tiers, projects compound growth over time,
and evaluates whether decisions serve the target growth rate.
Usage:
python growth-compass.py --current-value 1200 --previous-value 1000 --period weekly
python growth-compass.py --series "1000,1050,1100,1200,1350" --period weekly
python growth-compass.py --current-value 35000 --previous-value 32000 --period monthly --metric-name "MRR" --target-revenue 100000
python growth-compass.py --current-value 1200 --previous-value 1000 --period weekly --json
"""
import argparse
import json
import math
import sys
from typing import List, Optional, Tuple
# ---------------------------------------------------------------------------
# YC Growth Benchmarks
# ---------------------------------------------------------------------------
# Paul Graham's empirical benchmarks from "Startup = Growth" (2012)
# and YC's internal coaching targets during batch programs.
YC_BENCHMARKS = [
{"min_pct": 10.0, "label": "Outstanding", "color": "🟣",
"assessment": "Breakout trajectory — extremely rare. This is the Stripe/Coinbase zone."},
{"min_pct": 7.0, "label": "Very Good", "color": "🟢",
"assessment": "Exceptional progress. Top quartile of YC companies."},
{"min_pct": 5.0, "label": "Good", "color": "🟢",
"assessment": "Solid trajectory. YC's target zone — keep pushing."},
{"min_pct": 2.0, "label": "Below Average", "color": "🟡",
"assessment": "Below YC average. Need significant acceleration — likely haven't found product-market fit."},
{"min_pct": 0.0, "label": "Concerning", "color": "🔴",
"assessment": "Very low growth. Haven't figured out what you're doing."},
]
# Compound multipliers at various rates
COMPOUND_MULTIPLIERS = {
1: {"weekly": 1.01, "yearly": 1.68, "label": "Concerning"},
2: {"weekly": 1.02, "yearly": 2.81, "label": "Below Average"},
5: {"weekly": 1.05, "yearly": 12.64, "label": "Good"},
7: {"weekly": 1.07, "yearly": 33.73, "label": "Very Good"},
10: {"weekly": 1.10, "yearly": 142.04, "label": "Outstanding"},
}
def classify_growth(rate_pct: float) -> dict:
"""Classify a growth rate against YC benchmarks."""
for bench in YC_BENCHMARKS:
if rate_pct >= bench["min_pct"]:
return bench
return YC_BENCHMARKS[-1]
def period_normalizer(period: str) -> Tuple[str, int]:
"""Return (period_name, periods_per_year) for the given period."""
period = period.lower()
mapping = {
"weekly": ("weekly", 52),
"monthly": ("monthly", 12),
"quarterly": ("quarterly", 4),
}
if period not in mapping:
raise ValueError(f"Unknown period: {period}. Must be one of: weekly, monthly, quarterly")
return mapping[period]
def weekly_equivalent(rate_pct: float, from_period: str) -> float:
"""Convert a growth rate from any period to its weekly equivalent."""
if from_period == "weekly":
return rate_pct
periods_per_year = {"weekly": 52, "monthly": 12, "quarterly": 4}
n = periods_per_year[from_period]
# Convert: (1 + r_monthly)^(1/4.33) - 1 ≈ weekly rate
weekly_rate = (1 + rate_pct / 100) ** (1 / (n / 52)) - 1
return weekly_rate * 100
# ---------------------------------------------------------------------------
# Core calculations
# ---------------------------------------------------------------------------
def compute_growth_rate(current: float, previous: float) -> float:
"""Compute growth rate as a percentage."""
if previous <= 0:
return 0.0
return ((current - previous) / previous) * 100
def compute_series_rates(
values: List[float],
) -> Tuple[List[float], float, float, float]:
"""Compute growth rates from a time series of values.
Returns
-------
(period_rates, mean_rate, median_rate, cwgr)
where cwgr is the compound weekly growth rate fitted from first to last value.
"""
if len(values) < 2:
return [], 0.0, 0.0, 0.0
rates = []
for i in range(1, len(values)):
if values[i - 1] > 0:
rates.append(((values[i] - values[i - 1]) / values[i - 1]) * 100)
if not rates:
return [], 0.0, 0.0, 0.0
mean_rate = sum(rates) / len(rates)
sorted_rates = sorted(rates)
n = len(sorted_rates)
if n % 2 == 0:
median_rate = (sorted_rates[n // 2 - 1] + sorted_rates[n // 2]) / 2
else:
median_rate = sorted_rates[n // 2]
# Compound rate from first to last value
if len(values) >= 2 and values[0] > 0:
total_growth = values[-1] / values[0]
cwgr = (total_growth ** (1 / (len(values) - 1)) - 1) * 100
else:
cwgr = 0.0
return rates, mean_rate, median_rate, cwgr
def project_value(
current: float,
growth_rate_pct: float,
periods: int,
) -> float:
"""Project future value given a constant growth rate."""
return current * ((1 + growth_rate_pct / 100) ** periods)
def doubling_time(growth_rate_pct: float) -> float:
"""Compute the number of periods to double at the given growth rate."""
if growth_rate_pct <= 0:
return float("inf")
return math.log(2) / math.log(1 + growth_rate_pct / 100)
def time_to_target(
current: float,
target: float,
growth_rate_pct: float,
) -> Optional[float]:
"""Compute periods needed to reach target at given growth rate."""
if current >= target:
return 0.0
if growth_rate_pct <= 0:
return None
return math.log(target / current) / math.log(1 + growth_rate_pct / 100)
# ---------------------------------------------------------------------------
# Analysis
# ---------------------------------------------------------------------------
def analyze_growth(
current_value: float,
previous_value: Optional[float] = None,
series: Optional[List[float]] = None,
period: str = "weekly",
project_periods: int = 52,
target_value: Optional[float] = None,
metric_name: str = "users/revenue",
) -> dict:
"""Full growth analysis.
Parameters
----------
current_value : Current period's metric value
previous_value : Previous period's metric value (optional if series provided)
series : Full time series of values (optional, overrides previous_value)
period : 'weekly', 'monthly', or 'quarterly'
project_periods : Number of periods to project forward (default: 52)
target_value : Optional target metric to compute time-to-target
metric_name : Human-readable name for the metric
Returns
-------
dict with all computed fields
"""
period_name, periods_per_year = period_normalizer(period)
# Compute rate
if series and len(series) >= 2:
rates, mean_rate, median_rate, cwgr = compute_series_rates(series)
growth_rate = mean_rate if mean_rate != 0 else cwgr
series_info = {
"num_data_points": len(series),
"rates": [round(r, 2) for r in rates],
"mean_rate": round(mean_rate, 2),
"median_rate": round(median_rate, 2),
"cwgr": round(cwgr, 2),
"first_value": series[0],
"last_value": series[-1],
}
elif previous_value is not None:
growth_rate = compute_growth_rate(current_value, previous_value)
series_info = {
"num_data_points": 2,
"rate": round(growth_rate, 2),
}
else:
return {"error": "Either --previous-value or --series is required."}
# Classify
benchmark = classify_growth(growth_rate)
weekly_rate = weekly_equivalent(growth_rate, period)
weekly_benchmark = classify_growth(weekly_rate)
# Projections
projected_1yr = project_value(current_value, growth_rate, periods_per_year)
projected_2yr = project_value(current_value, growth_rate, periods_per_year * 2)
projected_N = project_value(current_value, growth_rate, project_periods)
# Doubling and target
double_p = doubling_time(growth_rate)
time_to_t = (time_to_target(current_value, target_value, growth_rate)
if target_value is not None else None)
# Growth rate tier table (what other rates would do)
tier_projections = {}
for rate_pct in [1, 2, 5, 7, 10]:
tier_projections[str(rate_pct)] = {
"label": COMPOUND_MULTIPLIERS.get(rate_pct, {}).get("label", ""),
"yearly_multiple": round((1 + rate_pct / 100) ** periods_per_year, 2),
"projected_1yr": round(project_value(current_value, rate_pct, periods_per_year), 2),
"doubling_periods": round(doubling_time(rate_pct), 1),
}
# Assessment text
if growth_rate >= 5:
assessment_text = (
f"At {growth_rate:.1f}% {period_name} growth, you're in YC's good-to-outstanding range. "
f"Keep pushing — compound growth at this rate transforms the business."
)
elif growth_rate >= 2:
assessment_text = (
f"At {growth_rate:.1f}% {period_name} growth, you're below YC's target zone. "
f"Paul Graham's advice: start doing things that don't scale. Recruit users manually, "
f"delight early customers, measure what works, and compound from there."
)
else:
assessment_text = (
f"At {growth_rate:.1f}% {period_name} growth, this is concerning. "
f"You haven't yet figured out what you're doing. Focus on finding something "
f"that a small number of users genuinely love — then grow from there."
)
result = {
"inputs": {
"current_value": current_value,
"previous_value": previous_value,
"metric_name": metric_name,
"period": period_name,
"project_periods": project_periods,
"target_value": target_value,
},
"series_info": series_info,
"growth_rate": {
"period_rate_pct": round(growth_rate, 2),
"weekly_equivalent_pct": round(weekly_rate, 2),
"period_name": period_name,
},
"benchmark": {
"label": benchmark["label"],
"icon": benchmark["color"],
"assessment": benchmark["assessment"],
"weekly_benchmark_label": weekly_benchmark["label"],
},
"projections": {
f"projected_{project_periods}_periods": round(projected_N, 2),
"projected_1_year": round(projected_1yr, 2),
"projected_2_years": round(projected_2yr, 2),
"doubling_time_periods": round(double_p, 1),
"time_to_target_periods": round(time_to_t, 1) if time_to_t is not None else None,
"target_value": target_value,
},
"tier_comparison": tier_projections,
"assessment_text": assessment_text,
}
return result
# ---------------------------------------------------------------------------
# Output formatting
# ---------------------------------------------------------------------------
def format_output(result: dict) -> str:
"""Format the analysis as a human-readable report."""
if "error" in result:
return f"Error: {result['error']}"
lines = []
inputs = result["inputs"]
rate = result["growth_rate"]
bench = result["benchmark"]
proj = result["projections"]
series = result["series_info"]
# Header
lines.append("=" * 60)
lines.append(f" WEEKLY GROWTH COMPASS — {bench['icon']} {bench['label']}")
lines.append("=" * 60)
lines.append("")
# Inputs
lines.append("── Inputs ──────────────────────────────────────────────")
lines.append(f" Metric: {inputs['metric_name']}")
lines.append(f" Current value: {inputs['current_value']:>10,.0f}")
if inputs.get("previous_value"):
lines.append(f" Previous value: {inputs['previous_value']:>10,.0f}")
lines.append(f" Period: {inputs['period']}")
lines.append(f" Data points: {series.get('num_data_points', 2)}")
lines.append("")
# Growth rate
lines.append("── Growth Rate ──────────────────────────────────────────")
lines.append(f" Period growth: {rate['period_rate_pct']:>7.2f}% ({rate['period_name']})")
lines.append(f" Weekly equiv: {rate['weekly_equivalent_pct']:>7.2f}%")
lines.append(f" YC Benchmark: {bench['icon']} {bench['label']}")
lines.append("")
if series.get("rates"):
rates = series["rates"]
lines.append(f" Period-over-period rates:")
for i, r in enumerate(rates):
arrows = "↑" if r > 0 else "↓" if r < 0 else "→"
lines.append(f" Period {i+1}-{i+2}: {r:>6.2f}% {arrows}")
lines.append(f" Mean rate: {series['mean_rate']:>7.2f}%")
lines.append(f" Median rate: {series['median_rate']:>7.2f}%")
lines.append(f" CWGR: {series['cwgr']:>7.2f}% (compound from first to last)")
lines.append("")
# Assessment
lines.append("── Assessment ───────────────────────────────────────────")
lines.append(f" {result['assessment_text']}")
lines.append("")
# Projections
lines.append("── Projections ─────────────────────────────────────────")
lines.append(f" Doubling time: {proj['doubling_time_periods']:>7.1f} {rate['period_name']} periods")
lines.append(f" Projected 1 year: {proj['projected_1_year']:>10,.0f}")
lines.append(f" Projected 2 years: {proj['projected_2_years']:>10,.0f}")
if proj.get("time_to_target_periods") is not None and proj.get("target_value"):
lines.append(f" Time to target: {proj['time_to_target_periods']:>7.1f} {rate['period_name']} periods")
lines.append(f" Target value: {proj['target_value']:>10,.0f}")
lines.append("")
# Tier comparison
lines.append("── Growth Rate Comparison ──────────────────────────────")
lines.append(f" {'Rate':>6} {'Label':>18} {'1-Year Multiple':>18} {'1-Year Value':>16} {'Double In':>12}")
lines.append(f" {'-'*6} {'-'*18} {'-'*18} {'-'*16} {'-'*12}")
for rate_pct_str, tier in result["tier_comparison"].items():
rate_pct = int(rate_pct_str)
marker = "◀" if rate_pct == round(rate["period_rate_pct"]) else ""
lines.append(
f" {rate_pct:>5}% {tier['label']:>18} "
f"{tier['yearly_multiple']:>17.1f}x "
f"{tier['projected_1yr']:>14,.0f} "
f"{tier['doubling_periods']:>7.1f}p {marker}"
)
lines.append("")
# Compass question
lines.append("── The Compass Question ────────────────────────────────")
lines.append(f" Your target growth rate: {rate['period_rate_pct']:.1f}% {rate['period_name']}")
lines.append(f" For every decision this week, ask:")
lines.append(f" \"Does this serve our {rate['period_rate_pct']:.1f}% {rate['period_name']} growth target?\"")
lines.append(f" If yes → do it. If no → defer it.")
lines.append("")
lines.append(f" At end of week, measure actual growth against target.")
lines.append(f" If you missed, something else matters more than what you did.")
lines.append("")
lines.append("=" * 60)
lines.append(" Paul Graham, \"Startup = Growth\" (September 2012)")
lines.append(" paulgraham.com/growth.html")
lines.append("=" * 60)
return "\n".join(lines)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Weekly Growth Compass — YC's growth rate framework",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python growth-compass.py --current-value 1200 --previous-value 1000 --period weekly
python growth-compass.py --series "1000,1050,1100,1200,1350" --period weekly
python growth-compass.py --current-value 35000 --previous-value 32000 --period monthly --metric-name "MRR" --target-revenue 100000
python growth-compass.py --current-value 1200 --previous-value 1000 --period weekly --json
""",
)
parser.add_argument("--current-value", type=float, help="Current period metric value")
parser.add_argument("--previous-value", type=float, help="Previous period metric value")
parser.add_argument("--series", type=str, help="Comma-separated time series (overrides --current/--previous)")
parser.add_argument("--period", type=str, default="weekly", choices=["weekly", "monthly", "quarterly"],
help="Period type (default: weekly)")
parser.add_argument("--project-periods", type=int, default=52,
help="Periods to project forward (default: 52)")
parser.add_argument("--target-value", type=float, help="Target metric value to compute time-to-target")
parser.add_argument("--metric-name", type=str, default="users/revenue",
help="Human-readable metric name (default: 'users/revenue')")
parser.add_argument("--json", action="store_true", help="Output as JSON")
parser.add_argument("--dry-run", action="store_true", help="Validate inputs and show what would be computed")
args = parser.parse_args()
# Parse series if provided
series = None
if args.series:
try:
series = [float(x.strip()) for x in args.series.split(",")]
except ValueError:
parser.error("--series must be comma-separated numbers")
if len(series) < 2:
parser.error("--series must have at least 2 values")
# Validate
if not series and args.current_value is None:
parser.error("Either --current-value (with --previous-value) or --series is required")
if not series and args.previous_value is None:
parser.error("--previous-value is required when using --current-value")
if args.current_value is not None and args.current_value < 0:
parser.error("--current-value must be >= 0")
if args.previous_value is not None and args.previous_value < 0:
parser.error("--previous-value must be >= 0")
if args.target_value is not None and args.target_value < 0:
parser.error("--target-value must be >= 0")
if args.project_periods < 1:
parser.error("--project-periods must be >= 1")
if args.dry_run:
if series:
print(json.dumps({
"status": "valid",
"data_points": len(series),
"first_value": series[0],
"last_value": series[-1],
"period": args.period,
}, indent=2))
else:
print(json.dumps({
"status": "valid",
"current_value": args.current_value,
"previous_value": args.previous_value,
"period": args.period,
}, indent=2))
return
current_val: float = series[-1] if series else (args.current_value or 0.0)
result = analyze_growth(
current_value=current_val,
previous_value=args.previous_value,
series=series,
period=args.period,
project_periods=args.project_periods,
target_value=args.target_value,
metric_name=args.metric_name,
)
if args.json:
print(json.dumps(result, indent=2))
else:
print(format_output(result))
if __name__ == "__main__":
main()