mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-13 04:26:28 +03:00
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).
492 lines
20 KiB
Python
Executable File
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()
|