Files
claude-skills-reference/c-level-advisor/chro-advisor/scripts/comp_benchmarker.py
Alireza Rezvani 466aa13a7b feat: C-Suite expansion — 8 new executive advisory roles (2→10) (#264)
* feat: C-Suite expansion — 8 new executive advisory roles

Add COO, CPO, CMO, CFO, CRO, CISO, CHRO advisors and Executive Mentor.
Expands C-level advisory from 2 to 10 roles with 74 total files.

Each role includes:
- SKILL.md (lean, <5KB, ~1200 tokens for context efficiency)
- Reference docs (loaded on demand, not at startup)
- Python analysis scripts (stdlib only, runnable CLI)

Executive Mentor features /em: slash commands (challenge, board-prep,
hard-call, stress-test, postmortem) with devil's advocate agent.

21 Python tools, 24 reference frameworks, 28,379 total lines.
All SKILL.md files combined: ~17K tokens (8.5% of 200K context window).

Badge: 88 → 116 skills

* feat: C-Suite orchestration layer + 18 complementary skills

ORCHESTRATION (new):
- cs-onboard: Founder interview → company-context.md
- chief-of-staff: Routing, synthesis, inter-agent orchestration
- board-meeting: 6-phase multi-agent deliberation protocol
- decision-logger: Two-layer memory (raw transcripts + approved decisions)
- agent-protocol: Inter-agent invocation with loop prevention
- context-engine: Company context loading + anonymization

CROSS-CUTTING CAPABILITIES (new):
- board-deck-builder: Board/investor update assembly
- scenario-war-room: Cascading multi-variable what-if modeling
- competitive-intel: Systematic competitor tracking + battlecards
- org-health-diagnostic: Cross-functional health scoring (8 dimensions)
- ma-playbook: M&A strategy (acquiring + being acquired)
- intl-expansion: International market entry frameworks

CULTURE & COLLABORATION (new):
- culture-architect: Values → behaviors, culture code, health assessment
- company-os: EOS/Scaling Up operating system selection + implementation
- founder-coach: Founder development, delegation, blind spots
- strategic-alignment: Strategy cascade, silo detection, alignment scoring
- change-management: ADKAR-based change rollout framework
- internal-narrative: One story across employees/investors/customers

UPGRADES TO EXISTING ROLES:
- All 10 roles get reasoning technique directives
- All 10 roles get company-context.md integration
- All 10 roles get board meeting isolation rules
- CEO gets stage-adaptive temporal horizons (seed→C)

Key design decisions:
- Two-layer memory prevents hallucinated consensus from rejected ideas
- Phase 2 isolation: agents think independently before cross-examination
- Executive Mentor (The Critic) sees all perspectives, others don't
- 25 Python tools total (stdlib only, no dependencies)

52 new files, 10 modified, 10,862 new lines.
Total C-suite ecosystem: 134 files, 39,131 lines.

* fix: connect all dots — Chief of Staff routes to all 28 skills

- Added complementary skills registry to routing-matrix.md
- Chief of Staff SKILL.md now lists all 28 skills in ecosystem
- Added integration tables to scenario-war-room and competitive-intel
- Badge: 116 → 134 skills
- README: C-Level Advisory count 10 → 28

Quality audit passed:
 All 10 roles: company-context, reasoning, isolation, invocation
 All 6 phases in board meeting
 Two-layer memory with DO_NOT_RESURFACE
 Loop prevention (no self-invoke, max depth 2, no circular)
 All /em: commands present
 All complementary skills cross-reference roles
 Chief of Staff routes to every skill in ecosystem

* refactor: CEO + CTO advisors upgraded to C-suite parity

Both roles now match the structural standard of all new roles:
- CEO: 11.7KB → 6.8KB SKILL.md (heavy content stays in references)
- CTO: 10KB → 7.2KB SKILL.md (heavy content stays in references)

Added to both:
- Integration table (who they work with and when)
- Key diagnostic questions
- Structured metrics dashboard table
- Consistent section ordering (Keywords → Quick Start → Responsibilities → Questions → Metrics → Red Flags → Integration → Reasoning → Context)

CEO additions:
- Stage-adaptive temporal horizons (seed=3m/6m/12m → B+=1y/3y/5y)
- Cross-references to culture-architect and board-deck-builder

CTO additions:
- Key Questions section (7 diagnostic questions)
- Structured metrics table (DORA + debt + team + architecture + cost)
- Cross-references to all peer roles

All 10 roles now pass structural parity:  Keywords  QuickStart  Questions  Metrics  RedFlags  Integration

* feat: add proactive triggers + output artifacts to all 10 roles

Every C-suite role now specifies:
- Proactive Triggers: 'surface these without being asked' — context-driven
  early warnings that make advisors proactive, not reactive
- Output Artifacts: concrete deliverables per request type (what you ask →
  what you get)

CEO: runway alerts, board prep triggers, strategy review nudges
CTO: deploy frequency monitoring, tech debt thresholds, bus factor flags
COO: blocker detection, scaling threshold warnings, cadence gaps
CPO: retention curve monitoring, portfolio dog detection, research gaps
CMO: CAC trend monitoring, positioning gaps, budget staleness
CFO: runway forecasting, burn multiple alerts, scenario planning gaps
CRO: NRR monitoring, pipeline coverage, pricing review triggers
CISO: audit overdue alerts, compliance gaps, vendor risk
CHRO: retention risk, comp band gaps, org scaling thresholds
Executive Mentor: board prep triggers, groupthink detection, hard call surfacing

This transforms the C-suite from reactive advisors into proactive partners.

* feat: User Communication Standard — structured output for all roles

Defines 3 output formats in agent-protocol/SKILL.md:

1. Standard Output: Bottom Line → What → Why → How to Act → Risks → Your Decision
2. Proactive Alert: What I Noticed → Why It Matters → Action → Urgency (🔴🟡)
3. Board Meeting: Decision Required → Perspectives → Agree/Disagree → Critic → Action Items

10 non-negotiable rules:
- Bottom line first, always
- Results and decisions only (no process narration)
- What + Why + How for every finding
- Actions have owners and deadlines ('we should consider' is banned)
- Decisions framed as options with trade-offs
- Founder is the highest authority — roles recommend, founder decides
- Risks are concrete (if X → Y, costs $Z)
- Max 5 bullets per section
- No jargon without explanation
- Silence over fabricated updates

All 10 roles reference this standard.
Chief of Staff enforces it as a quality gate.
Board meeting Phase 4 uses the Board Meeting Output format.

* feat: Internal Quality Loop — verification before delivery

No role presents to the founder without passing verification:

Step 1: Self-Verification (every role, every time)
  - Source attribution: where did each data point come from?
  - Assumption audit: [VERIFIED] vs [ASSUMED] tags on every finding
  - Confidence scoring: 🟢 high / 🟡 medium / 🔴 low per finding
  - Contradiction check against company-context + decision log
  - 'So what?' test: every finding needs a business consequence

Step 2: Peer Verification (cross-functional)
  - Financial claims → CFO validates math
  - Revenue projections → CRO validates pipeline backing
  - Technical feasibility → CTO validates
  - People/hiring impact → CHRO validates
  - Skip for single-domain, low-stakes questions

Step 3: Critic Pre-Screen (high-stakes only)
  - Irreversible decisions, >20% runway impact, strategy changes
  - Executive Mentor finds weakest point before founder sees it
  - Suspicious consensus triggers mandatory pre-screen

Step 4: Course Correction (after founder feedback)
  - Approve → log + assign actions
  - Modify → re-verify changed parts
  - Reject → DO_NOT_RESURFACE + learn why
  - 30/60/90 day post-decision review

Board meeting contributions now require self-verified format with
confidence tags and source attribution on every finding.

* fix: resolve PR review issues 1, 4, and minor observation

Issue 1: c-level-advisor/CLAUDE.md — completely rewritten
  - Was: 2 skills (CEO, CTO only), dated Nov 2025
  - Now: full 28-skill ecosystem map with architecture diagram,
    all roles/orchestration/cross-cutting/culture skills listed,
    design decisions, integration with other domains

Issue 4: Root CLAUDE.md — updated all stale counts
  - 87 → 134 skills across all 3 references
  - C-Level: 2 → 33 (10 roles + 5 mentor commands + 18 complementary)
  - Tool count: 160+ → 185+
  - Reference count: 200+ → 250+

Minor observation: Documented plugin.json convention
  - Explained in c-level-advisor/CLAUDE.md that only executive-mentor
    has plugin.json because only it has slash commands (/em: namespace)
  - Other skills are invoked by name through Chief of Staff or directly

Also fixed: README.md 88+ → 134 in two places (first line + skills section)

* fix: update all plugin/index registrations for 28-skill C-suite

1. c-level-advisor/.claude-plugin/plugin.json — v2.0.0
   - Was: 2 skills, generic description
   - Now: all 28 skills listed with descriptions, all 25 scripts,
     namespace 'cs', full ecosystem description

2. .codex/skills-index.json — added 18 complementary skills
   - Was: 10 roles only
   - Now: 28 total c-level entries (10 roles + 6 orchestration +
     6 cross-cutting + 6 culture)
   - Each with full description for skill discovery

3. .claude-plugin/marketplace.json — updated c-level-skills entry
   - Was: generic 2-skill description
   - Now: v2.0.0, full 28-skill ecosystem description,
     skills_count: 28, scripts_count: 25

* feat: add root SKILL.md for c-level-advisor ClawHub package

---------

Co-authored-by: Leo <leo@openclaw.ai>
2026-03-06 01:35:08 +01:00

614 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 # 15
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.951.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.850.94 (developing)": 0,
"0.951.05 (target zone)": 0,
"1.061.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.850.94 (developing)"] += 1
elif cr <= 1.05:
buckets["0.951.05 (target zone)"] += 1
elif cr <= 1.15:
buckets["1.061.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"${int(n):,.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()