CHRO Advisor
Human resources leadership — hiring strategy, compensation benchmarking, org structure design, culture development, and people operations at scale.
What this skill does
Transform your business goals into a scalable people strategy that aligns hiring plans, compensation structures, and organizational structure with your current growth stage. Build cost-projected headcount budgets, competitive salary bands, and performance frameworks that help retain top talent. Use this advisor when you are scaling your team, restructuring departments, or need expert HR strategy without hiring a full-time executive.
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
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/hybridreferences/comp_frameworks.md— salary bands, equity, total comp modeling, raise/refresh processreferences/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.mdbefore 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]
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):
- Radford / Aon — Gold standard. Expensive ($10K+/year). Worth it at Series B+.
- Levels.fyi — Excellent for engineering. Free. Self-reported but large sample.
- Glassdoor Salary — Broad coverage. Less precise for startups.
- Pave / Carta Total Comp — VC-backed companies. Good peer benchmarking.
- LinkedIn Salary — Free tier. Reasonable signal for G&A roles.
- 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:
- Anniversary grant: Annual cliff-free refresh for all employees above a performance threshold
- Evergreen model: Continuous vesting maintained — refresh annually so employee always has 2–3 years remaining
- 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 yearsExample: 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:
- Manager nominates with written business case
- Calibration committee reviews cross-functionally
- HR validates against band (no off-band exceptions without CHRO sign-off)
- 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:
- Match — Risk: signals you underpay; sets precedent
- Partial match — "We can do X, which is the top of your band" — cleaner
- 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:
- Pull all employee data: level, function, salary, tenure, performance ratings, gender, ethnicity
- Run regression controlling for level, tenure, and performance
- Unexplained gap after controls = the problem to fix
- 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.
Org Design Reference
Spans of control, layering decisions, reorgs, title frameworks, career ladders, and the founder→professional management transition.
Core Org Design Principles
- Structure follows strategy. Reorg after strategy shifts, not before.
- Optimize for the bottleneck. Where does work get slow? Design around that.
- Minimize coordination cost. Conway's Law: your org structure becomes your product architecture. Design intentionally.
- Bias toward flatness until it breaks. Adding layers adds cost and slows decisions.
- 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 DesignersBest 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 + teamBest 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
- Retention: Employees can see where they're going
- Consistency: Managers use the same criteria for promotions
- Compensation: Bands anchor to levels; levels require definitions
- 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
- Interview your best performers — "What do you do that your junior peers don't?" Collect behaviors, not aspirations.
- 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.
- Manager calibration — Every manager rates 5 current employees against the draft. Gaps surface immediately.
- 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)
- Affected individuals first (private, before anything else)
- Affected managers second (to prepare for team conversations)
- Full team/company third (all-hands or company note)
- 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)
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):
- 30-min intro call (culture/motivation fit, comp alignment)
- Take-home or live work sample (2–4 hours max, paid for senior roles)
- 60-min deep-dive with founders
- 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):
- VP/Head of Engineering (if founder isn't technical)
- Head of Product
- First dedicated recruiter (when you're hiring > 10/year)
- First Finance/Operations hire
- 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)
- Manager quality — Gallup: 70% of team engagement variance is explained by the manager. Fix managers first.
- Growth trajectory — People leave when they can't see their next role. Career ladders are retention tools.
- Compensation competitiveness — Being at P25 on salary is a slow leak. Audit annually.
- Mission/product belief — Especially for senior ICs. They want to work on something that matters.
- Team quality — "I stay because of the people I work with." True at every level.
- 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:
- Managers submit preliminary ratings independently
- HR facilitates 2-hr calibration with all managers in a function
- Managers must justify outliers (top and bottom)
- Ratings adjusted for consistency
- 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:
Anchor days with purpose — Office days should have things that require the office: workshops, team rituals, whiteboarding sessions. Not just "presence."
Async-first culture, not async-only — Document decisions. Write things down. Use Loom for walkthroughs. Reduce "quick sync" meetings.
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.
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.
#!/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"${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()
#!/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 ${totals['revenue_per_employee_current']:,.0f} to "
f"${totals['revenue_per_employee_target']:,.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 ${lp_cost:,.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"${totals['total_recruiter_fees']:,.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"${n:,.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()
Install this Skill
Skills give your AI agent a consistent, structured approach to this task — better output than a one-off prompt.
npx skills add alirezarezvani/claude-skills --skill c-level-advisor/chro-advisor Community skill by @alirezarezvani. Need a walkthrough? See the install guide →
Works with
Prefer no terminal? Download the ZIP and place it manually.
Details
- Category
- Leadership
- License
- MIT
- Author
- @alirezarezvani
- Source
- GitHub →
- Source file
-
show path
c-level-advisor/chro-advisor/SKILL.md
People who install this also use
CEO Advisor
Executive leadership coaching — strategic decision-making, organizational development, board governance, and navigating high-stakes business challenges.
@alirezarezvani
Culture Architect
Build company culture as operational behavior — define values-to-actions mappings, design culture rituals, and diagnose cultural drift before it becomes a problem.
@alirezarezvani
Org Health Diagnostic
An 8-dimension organizational health check — identifies dysfunction, misalignment, and capability gaps before they become crises.
@alirezarezvani