
Cost Estimate
- 1 installs
- 13 repo stars
- Updated April 14, 2026
- ulpi-io/browse
Helps with ai & agent building tasks.
About
cost-estimate is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- cost-estimate
- AI & Agent Building
- AI-coding skill
Cost Estimate by the numbers
- 1 all-time installs (skills.sh)
- Ranked #14,102 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ulpi-io/browse --skill cost-estimateAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 13 |
| Last updated | April 14, 2026 |
| Repository | ulpi-io/browse ↗ |
What it does
Helps with ai & agent building tasks.
Files
Cost Estimate Command
You are a senior software engineering consultant tasked with estimating the development cost of code in the current repository.
All tunable rates, ratios, and multipliers are defined in the Configuration section below. When performing calculations in later steps, always reference these values — do not use hardcoded numbers elsewhere. To customize the estimate for a different market, team structure, or role mix, edit only this section.
Configuration
All cost parameters in one place. Edit these to adjust the entire estimate.
Coding Productivity Rates (lines/hour, pure focused output)
| Code Category | Low | High | Examples |
|---|---|---|---|
| Simple CRUD/UI/boilerplate | 50 | 80 | Forms, lists, repetitive layouts, config screens |
| Standard views with logic | 35 | 55 | Typical screens, moderate complexity views |
| Complex UI (animations, custom) | 25 | 40 | Onboarding flows, custom components, transitions |
| Business logic / API clients | 30 | 50 | Networking, state management, data transforms |
| Database/persistence | 30 | 50 | CRUD, migrations, queries, schema definitions |
| Audio/video processing | 20 | 30 | AV pipelines, streaming, encoding/decoding |
| GPU/shader programming | 15 | 25 | Metal, CUDA, render pipelines, compute shaders |
| Native C/C++ interop | 15 | 25 | FFI, bridging, unsafe code, native plugins |
| System extensions/plugins | 15 | 25 | OS extensions, daemons, drivers, kernel modules |
| On-device ML inference | 15 | 25 | CoreML, MLX, ONNX, model integration |
| Tests | 50 | 80 | Tests are boilerplate-heavy with assertions |
| Config/build files | 40 | 60 | Build configs, CI/CD, manifests, project files |
| Documentation | 60 | 100 | Markdown, READMEs, API docs, comments-only files |
Development Overhead Multipliers (% of base coding hours)
| Overhead Category | Low | High | Notes |
|---|---|---|---|
| Architecture & design | 12% | 15% | Upfront design, API contracts, data modeling |
| Debugging & troubleshooting | 20% | 25% | Bug fixing, edge cases, platform quirks |
| Code review & refactoring | 8% | 12% | PR reviews, cleanup passes, tech debt |
| Documentation | 5% | 8% | Inline docs, README updates, API docs |
| Integration & testing | 15% | 18% | Wiring components, end-to-end testing |
| Learning curve | 8% | 15% | New frameworks, APIs, unfamiliar domains |
Total overhead range: ~68-93%
Hourly Market Rates by Role (USD, 2025 US market)
| Role | Low | Mid | High | Notes |
|---|---|---|---|---|
| Senior Engineer (generalist) | 100 | 150 | 225 | IC5+ full-stack / backend / mobile |
| Senior Engineer (specialist) | 125 | 175 | 250 | GPU, ML, systems, AV, security |
| Product Management | 125 | 160 | 200 | PRDs, roadmap, stakeholder mgmt |
| UX/UI Design | 100 | 140 | 175 | Wireframes, mockups, design systems |
| Engineering Management | 150 | 185 | 225 | 1:1s, hiring, performance, strategy |
| QA/Testing | 75 | 100 | 125 | Test plans, manual testing, automation |
| Project/Program Management | 100 | 125 | 150 | Schedules, dependencies, status |
| Technical Writing | 75 | 100 | 125 | User docs, API docs, internal docs |
| DevOps/Platform | 125 | 160 | 200 | CI/CD, infra, deployments |
Role Ratios (hours as % of engineering hours, by company stage)
| Role | Solo | Lean Startup | Growth Co | Enterprise |
|---|---|---|---|---|
| Product Management | 0% | 15% | 30% | 40% |
| UX/UI Design | 0% | 15% | 25% | 35% |
| Engineering Management | 0% | 5% | 15% | 20% |
| QA/Testing | 0% | 5% | 20% | 25% |
| Project/Program Management | 0% | 0% | 10% | 15% |
| Technical Writing | 0% | 0% | 5% | 10% |
| DevOps/Platform | 0% | 5% | 15% | 20% |
| Full Team Multiplier | 1.0x | ~1.45x | ~2.2x | ~2.65x |
Organizational Efficiency (coding hours as % of 40-hr week)
| Company Type | Efficiency | Effective Coding Hrs/Week |
|---|---|---|
| Solo/Startup (lean) | 65% | 26 |
| Growth Company | 55% | 22 |
| Enterprise | 45% | 18 |
| Large Bureaucracy | 35% | 14 |
Sanity Check Bounds
| Metric | Too Conservative | Target Range | Too Aggressive |
|---|---|---|---|
| Effective lines/hour (LOC / total hours) | < 12 | 15-30 | > 40 |
Claude ROI Constants
| Parameter | Value | Notes |
|---|---|---|
| Claude coding speed | 200-500 lines/hr | Fallback when no git history |
| Claude coding speed (midpoint) | 350 lines/hr | Used for LOC-based hour estimate |
| Human baseline rate for comparison | 150 $/hr | Senior engineer, used in savings calc |
| Claude subscription range | $20-200/month | Pro to Team plans |
---
Helper Scripts
Three Python scripts in .claude/skills/cost-estimate/helpers/ automate the heavy lifting. Use these instead of manual `find`, `wc -l`, `git log`, and inline math. They work on any repo.
1. loc_counter.py — Count lines of code
# Full repo (respects .gitignore via git ls-files)
python3 .claude/skills/cost-estimate/helpers/loc_counter.py
# Branch diff (added lines only)
python3 .claude/skills/cost-estimate/helpers/loc_counter.py --branch feat/foo
# Branch diff against specific base
python3 .claude/skills/cost-estimate/helpers/loc_counter.py --branch feat/foo --base develop
# Single commit
python3 .claude/skills/cost-estimate/helpers/loc_counter.py --commit abc1234Output (JSON): totals (lines, files, test/doc/config/source breakdown), by_language, by_directory, all_files (with path, lines, category, is_test, is_doc, is_config flags).
2. git_session_analyzer.py — Estimate Claude active hours
# All commits on current branch
python3 .claude/skills/cost-estimate/helpers/git_session_analyzer.py
# Specific branch
python3 .claude/skills/cost-estimate/helpers/git_session_analyzer.py --branch feat/fooOutput (JSON): total_commits, total_sessions, estimated_active_hours, sessions[] with date/start/end/commits/estimated_hours/subjects. Review the session estimates and adjust upward for large-scope commits (e.g. a single commit that adds 5000 lines should count as more than 1 hour).
3. cost_calculator.py — Calculate costs from categorized LOC
# Pipe categories as JSON
echo '{"audio_video_processing": 2000, "business_logic": 5000, ...}' | \
python3 .claude/skills/cost-estimate/helpers/cost_calculator.py --rate 150 --claude-hours 29Valid category keys: simple_crud_ui_boilerplate, standard_views, complex_ui, business_logic, database_persistence, audio_video_processing, gpu_shader, native_interop, system_extensions, on_device_ml, tests, config_build, documentation
Output (JSON): base_coding (rows with category/lines/rate/hours), overhead (rows), total_estimated_hours, sanity_check, calendar_time, engineering_cost, team_costs (per stage with role breakdowns), claude_roi (if --claude-hours given).
4. report_generator.py — Generate markdown report sections
# Full report from calculator + session data
python3 .claude/skills/cost-estimate/helpers/report_generator.py \
--calc costs.json --sessions sessions.json --project "MyApp" --scope "Full codebase"
# Single section only
python3 .claude/skills/cost-estimate/helpers/report_generator.py \
--calc costs.json --section executive_summary
# Pipe directly from calculator
echo '{"business_logic": 5000}' | python3 cost_calculator.py --rate 150 --claude-hours 29 | \
python3 report_generator.py --project "MyApp"Available sections: executive_summary, development_time, calendar_time, engineering_cost, team_cost, grand_total, claude_roi, assumptions
Output: Ready-to-paste markdown. Review, add complexity factors, market research rationale, and codebase metrics (which are project-specific and come from Step 1).
Recommended Workflow
# 1. Count LOC
python3 .claude/skills/cost-estimate/helpers/loc_counter.py > /tmp/loc.json
# 2. Analyze git sessions
python3 .claude/skills/cost-estimate/helpers/git_session_analyzer.py > /tmp/sessions.json
# 3. Classify files into categories (done manually from loc.json)
# Then pipe to calculator:
echo '{"category": lines, ...}' | \
python3 .claude/skills/cost-estimate/helpers/cost_calculator.py \
--rate 150 --claude-hours 29 > /tmp/calc.json
# 4. Generate report sections
python3 .claude/skills/cost-estimate/helpers/report_generator.py \
--calc /tmp/calc.json --sessions /tmp/sessions.json --project "MyApp"The main job is: classify files into categories (the creative/judgment part) and add context (complexity factors, market research, codebase description). The math and formatting are handled by the scripts.
---
Step 0: Determine Scope
Check the user's input for scope specifiers:
- No arguments (default): Estimate the entire codebase.
- `branch:<name>`: Estimate only the diff introduced by that branch (added lines only).
- `commit:<hash>`: Estimate only the diff for that single commit (added lines only).
The loc_counter.py script handles all three modes via --branch and --commit flags.
Step 1: Analyze the Codebase
Run `loc_counter.py` to get a complete breakdown. Then review the output to:
1. Identify the primary languages from by_language 2. Understand the directory structure from by_directory 3. Note test/doc/config splits from totals 4. Identify complexity factors — scan the all_files list for signs of advanced work (GPU code, system extensions, audio/video pipelines, ML inference, native interop, complex UI, etc.) 5. Detect project name from the repo directory name or top-level config files
Step 2: Classify and Calculate Development Hours
Review the all_files output from Step 1 and classify each file (or group of files) into categories from the Coding Productivity Rates table. Build a JSON object mapping category keys to line counts, then pipe it to `cost_calculator.py`.
These rates represent pure focused coding output — fingers on keyboard, writing code with modern IDE autocomplete. All thinking, debugging, reviewing, and design time is captured by the overhead multipliers — do not bake overhead into rates or it will be double-counted.
The calculator handles: base hours, overhead multipliers, sanity check, calendar time, engineering cost, and full team costs. Review the sanity_check in the output — if it fails, adjust your category assignments.
Every source line must be assigned to exactly one category. Do not double-count.
Step 3: Market Rates
Start with the Hourly Market Rates by Role from the Configuration section as baseline defaults.
Ask the user: "Use built-in market rates, or search the web for current rates for your tech stack/region?"
- Built-in rates — use the Configuration section defaults as-is (faster, no web dependency)
- Web research — use WebSearch to validate or adjust for:
- The specific tech stack detected in Step 1
- Geographic variations (US markets: SF Bay Area, NYC, Austin, Remote)
- Contractor vs. employee rates
If the user chooses web research, search for:
- "senior full stack developer hourly rate 2025"
- "senior software engineer hourly rate United States 2025"
- "[detected language/platform] developer contractor rate 2025"
If web search results differ significantly from the config defaults, note the discrepancy and use the researched rates. Otherwise, use the config defaults.
Step 4: Calculate Organizational Overhead
Real companies don't have developers coding 40 hours/week. Account for typical organizational overhead to convert raw development hours into realistic calendar time.
Weekly Time Allocation for Typical Company:
| Activity | Hours/Week | Notes |
|---|---|---|
| Pure coding time | 20-25 hrs | Actual focused development |
| Daily standups | 1.25 hrs | 15 min x 5 days |
| Weekly team sync | 1-2 hrs | All-hands, team meetings |
| 1:1s with manager | 0.5-1 hr | Weekly or biweekly |
| Sprint planning/retro | 1-2 hrs | Per week average |
| Code reviews (giving) | 2-3 hrs | Reviewing teammates' work |
| Slack/email/async | 3-5 hrs | Communication overhead |
| Context switching | 2-4 hrs | Interruptions, task switching |
| Ad-hoc meetings | 1-2 hrs | Unplanned discussions |
| Admin/HR/tooling | 1-2 hrs | Timesheets, tools, access requests |
Use the Organizational Efficiency table from the Configuration section for coding hours per week by company type.
Calendar Weeks Calculation:
Calendar Weeks = Raw Dev Hours / Effective Coding Hrs/Week (from config)Step 5: Calculate Full Team Cost
Engineering doesn't ship products alone. Use the Role Ratios and Hourly Market Rates by Role from the Configuration section to calculate the fully-loaded team cost.
For each company stage: 1. Look up the role ratio % from the config's Role Ratios table 2. Multiply engineering hours by that % to get each role's hours 3. Multiply each role's hours by the Mid rate from the Hourly Market Rates table 4. Sum all roles for Full Team Cost, or use the Full Team Multiplier shortcut
Calculation:
Full Team Cost = Engineering Cost x Full Team Multiplier (from config)Step 6: Generate Cost Estimate
Detect the project name from the repository (directory name, package manifest, or top-level config).
IMPORTANT: The report MUST lead with the Executive Summary and Claude ROI at the very top. The detailed breakdowns come after. This is the required report structure:
---
[Project Name] - Development Cost Estimate
Analysis Date: [Current Date] Scope: [Full codebase / Branch <name> (diff from <base>) / Commit <hash>]
---
Executive Summary
| Metric | Value |
|---|---|
| Codebase | [X] lines of [language] across [X] files |
| Engineering hours | [X] hours |
| Engineering cost (avg) | $[X,XXX] |
| Full team cost (Growth Co) | $[X,XXX] |
| Calendar time (solo dev) | ~[X] months |
Claude ROI
| Metric | Value |
|---|---|
| Claude active hours | ~[X] hours (across [X] calendar days) |
| Speed multiplier | [X]x faster than human developer |
| Value per Claude hour | $[X,XXX]/hr (engineering) |
| ROI | [X]x ($[X]k value for ~$[X] in Claude costs) |
Claude worked ~[X] hours and produced $[X] of professional development value = $[X,XXX] per Claude hour
---
Grand Total Summary
| Metric | Solo | Lean Startup | Growth Co | Enterprise |
|---|---|---|---|---|
| Calendar Time | [X] | [X] | [X] | [X] |
| Total Human Hours | [X] | [X] | [X] | [X] |
| Total Cost | $[X] | $[X] | $[X] | $[X] |
---
Detailed breakdown follows.
---
Codebase Metrics
- Total Lines of Code: [number] ([scope context: "in repository" or "in diff"])
- [Language 1]: [number] lines
- [Language 2]: [number] lines
- Tests: [number] lines
- Config/Build: [number] lines
- Documentation: [number] lines
- Complexity Factors:
- [Auto-detected factor 1, e.g. "Audio/video processing pipeline"]
- [Auto-detected factor 2, e.g. "System extension architecture"]
- [Auto-detected factor 3, e.g. "Third-party API integrations"]
Development Time Estimate
Base Development Hours: [number] hours
| Code Category | Lines | Rate (lines/hr) | Hours |
|---|---|---|---|
| [Category 1] | [X] | [X] | [X] |
| [Category 2] | [X] | [X] | [X] |
| ... | ... | ... | ... |
| Total Base | [X] | [X] |
Overhead Multipliers:
- Architecture & Design: +[X]% ([hours] hours)
- Debugging & Troubleshooting: +[X]% ([hours] hours)
- Code Review & Refactoring: +[X]% ([hours] hours)
- Documentation: +[X]% ([hours] hours)
- Integration & Testing: +[X]% ([hours] hours)
- Learning Curve: +[X]% ([hours] hours)
Total Estimated Hours: [number] hours
Sanity Check: [total LOC] / [total hours] = [X] effective lines/hour [PASS: within 15-30 range / ADJUST: outside range, explain adjustment]
Realistic Calendar Time (with Organizational Overhead)
| Company Type | Efficiency | Coding Hrs/Week | Calendar Weeks | Calendar Time |
|---|---|---|---|---|
| Solo/Startup (lean) | 65% | 26 hrs | [X] weeks | ~[X] months |
| Growth Company | 55% | 22 hrs | [X] weeks | ~[X] years |
| Enterprise | 45% | 18 hrs | [X] weeks | ~[X] years |
| Large Bureaucracy | 35% | 14 hrs | [X] weeks | ~[X] years |
Market Rate Research
Senior Developer Rates (2025):
- Low end: $[X]/hour (remote, mid-level market)
- Average: $[X]/hour (standard US market)
- High end: $[X]/hour (SF Bay Area, NYC, specialized)
Recommended Rate for This Project: $[X]/hour
Rationale: [Based on detected tech stack complexity and specialization requirements]
Total Cost Estimate (Engineering Only)
| Scenario | Hourly Rate | Total Hours | Total Cost |
|---|---|---|---|
| Low-end | $[X] | [hours] | $[X,XXX] |
| Average | $[X] | [hours] | $[X,XXX] |
| High-end | $[X] | [hours] | $[X,XXX] |
Recommended Estimate (Engineering Only): $[X,XXX] - $[X,XXX]
Full Team Cost (All Roles)
| Company Stage | Team Multiplier | Engineering Cost | Full Team Cost |
|---|---|---|---|
| Solo/Founder | 1.0x | $[X] | $[X] |
| Lean Startup | 1.45x | $[X] | $[X] |
| Growth Company | 2.2x | $[X] | $[X] |
| Enterprise | 2.65x | $[X] | $[X] |
Role Breakdown (Growth Company Example):
| Role | Hours | Rate | Cost |
|---|---|---|---|
| Engineering | [X] hrs | $[X]/hr | $[X] |
| Product Management | [X] hrs | $[X]/hr | $[X] |
| UX/UI Design | [X] hrs | $[X]/hr | $[X] |
| Engineering Management | [X] hrs | $[X]/hr | $[X] |
| QA/Testing | [X] hrs | $[X]/hr | $[X] |
| Project Management | [X] hrs | $[X]/hr | $[X] |
| Technical Writing | [X] hrs | $[X]/hr | $[X] |
| DevOps/Platform | [X] hrs | $[X]/hr | $[X] |
| TOTAL | [X] hrs | $[X] |
Claude ROI Analysis (Detailed)
Project Timeline:
- First commit / project start: [date]
- Latest commit: [date]
- Total calendar time: [X] days ([X] weeks)
Claude Active Hours Estimate:
- Total sessions identified: [X] sessions
- Estimated active hours: [X] hours
- Method: [git clustering / file timestamps / LOC estimate]
Value per Claude Hour:
| Value Basis | Total Value | Claude Hours | $/Claude Hour |
|---|---|---|---|
| Engineering only | $[X] | [X] hrs | $[X,XXX]/Claude hr |
| Full team (Growth Co) | $[X] | [X] hrs | $[X,XXX]/Claude hr |
Speed vs. Human Developer:
- Estimated human hours for same work: [X] hours
- Claude active hours: [X] hours
- Speed multiplier: [X]x (Claude was [X]x faster)
Cost Comparison:
- Human developer cost: $[X] (at config baseline rate)
- Estimated Claude cost: $[X] (subscription + API)
- Net savings: $[X]
- ROI: [X]x (every $1 spent on Claude produced $[X] of value)
Assumptions
1. Rates based on US market averages (2025) 2. Full-time equivalent allocation for all roles 3. Does not include:
- Marketing & sales
- Legal & compliance
- Office/equipment
- Hosting/infrastructure
- Ongoing maintenance post-launch
---
Step 7: Calculate Claude ROI — Value Per Claude Hour
This is the most important metric for understanding AI-assisted development efficiency. It answers: "What did each hour of Claude's actual working time produce?"
IMPORTANT: The Claude ROI results must appear in TWO places in the report: 1. Executive Summary at the very top (compact table format) 2. Claude ROI Analysis (Detailed) section with full breakdown
Calculate all ROI values in this step, then populate both sections when writing the report.
7a: Determine Actual Claude Clock Time
Run `git_session_analyzer.py` to automatically cluster commits into sessions and estimate active hours:
python3 .claude/skills/cost-estimate/helpers/git_session_analyzer.pyReview the output sessions and adjust estimates upward for commits with large scope (e.g. a single commit adding thousands of lines likely took 2-4 hours, not 1 hour). Use git show <hash> --stat to check the scope of low-commit sessions.
Fallback (no git): Estimate from LOC using the Claude ROI Constants: Claude active hours = Total LOC / 350 lines/hr
7b: Calculate ROI
Pass --claude-hours to cost_calculator.py (in Step 2) to get the full ROI breakdown automatically. The calculator computes speed multiplier, value per Claude hour, cost comparison, and savings.
---
Notes
Present the estimate in a clear, professional format suitable for sharing with stakeholders. Include confidence intervals and key assumptions. Highlight areas of highest complexity that drive cost.
IMPORTANT — Dollar Sign Escaping: Always escape $ as \$ in the final markdown report. Bare $ characters are interpreted as LaTeX math delimiters by many markdown renderers (GitHub, VS Code, etc.), which mangles currency values. The report_generator.py fmt() function handles this automatically, but when writing prose sections manually (e.g., market rate research, rationale text, assumptions), always use \$ for currency.
#!/usr/bin/env python3
"""
Cost Calculator — Calculate development cost from categorized LOC data.
Part of the cost-estimate agent toolchain. Takes a JSON mapping of {category: lines}
and applies the rate tables and overhead multipliers from the cost-estimate agent
configuration. Outputs a complete cost breakdown as JSON.
Works with any codebase -- categories are generic.
Usage:
echo '{"audio_video_processing": 2000, "standard_views": 5000}' | python3 cost_calculator.py
python3 cost_calculator.py --input categories.json
python3 cost_calculator.py --input categories.json --rate 175
python3 cost_calculator.py --input categories.json --claude-hours 29
Valid Category Keys (matching Coding Productivity Rates from agent config):
simple_crud_ui_boilerplate 50-80 lines/hr Forms, lists, repetitive layouts
standard_views 35-55 lines/hr Typical screens, moderate complexity
complex_ui 25-40 lines/hr Onboarding, custom components, animations
business_logic 30-50 lines/hr Networking, state management, data transforms
database_persistence 30-50 lines/hr CRUD, migrations, queries, schema
audio_video_processing 20-30 lines/hr AV pipelines, streaming, encoding
gpu_shader 15-25 lines/hr Metal, CUDA, render pipelines
native_interop 15-25 lines/hr FFI, bridging, unsafe code
system_extensions 15-25 lines/hr OS extensions, daemons, hotkeys
on_device_ml 15-25 lines/hr CoreML, MLX, ONNX, model integration
tests 50-80 lines/hr Tests (boilerplate-heavy)
config_build 40-60 lines/hr Build configs, CI/CD, manifests
documentation 60-100 lines/hr Markdown, READMEs, API docs
Overhead Multipliers Applied (% of base coding hours):
Architecture & Design 12-15%
Debugging & Troubleshooting 20-25%
Code Review & Refactoring 8-12%
Documentation 5-8%
Integration & Testing 15-18%
Learning Curve 8-15%
Total overhead: ~68-93% (midpoint ~80.5%)
Role Ratios by Company Stage (hours as % of engineering hours):
Solo: 1.0x (engineering only)
Lean Startup: ~1.45x (adds PM, UX, DevOps at 5-15%)
Growth Co: ~2.2x (adds QA, ProjMgmt, TechWriting at 5-30%)
Enterprise: ~2.65x (all roles at 10-40%)
Output JSON structure:
{
"base_coding": { "rows": [...], "total_lines": int, "total_hours": float },
"overhead": { "rows": [...], "total_percentage": float, "total_hours": float },
"total_estimated_hours": int,
"sanity_check": { "effective_lines_per_hour": float, "status": "PASS"|"ADJUST" },
"calendar_time": { "solo": {...}, "growth": {...}, ... },
"engineering_cost": { "low": {...}, "mid": {...}, "high": {...} },
"team_costs": { "solo": {...}, "growth_company": {...}, ... },
"claude_roi": { ... } // only if --claude-hours provided
}
"""
import argparse
import json
import math
import sys
# ============================================================
# CONFIGURATION: Coding Productivity Rates (lines/hour)
# These match the cost-estimate agent's configuration exactly.
# ============================================================
RATE_TABLE = {
"simple_crud_ui_boilerplate": {"low": 50, "high": 80, "label": "Simple CRUD/UI/boilerplate"},
"standard_views": {"low": 35, "high": 55, "label": "Standard views with logic"},
"complex_ui": {"low": 25, "high": 40, "label": "Complex UI (animations, custom)"},
"business_logic": {"low": 30, "high": 50, "label": "Business logic / API clients"},
"database_persistence": {"low": 30, "high": 50, "label": "Database/persistence"},
"audio_video_processing": {"low": 20, "high": 30, "label": "Audio/video processing"},
"gpu_shader": {"low": 15, "high": 25, "label": "GPU/shader programming"},
"native_interop": {"low": 15, "high": 25, "label": "Native C/C++ interop"},
"system_extensions": {"low": 15, "high": 25, "label": "System extensions/plugins"},
"on_device_ml": {"low": 15, "high": 25, "label": "On-device ML inference"},
"tests": {"low": 50, "high": 80, "label": "Tests"},
"config_build": {"low": 40, "high": 60, "label": "Config/build files"},
"documentation": {"low": 60, "high": 100, "label": "Documentation"},
}
# ============================================================
# CONFIGURATION: Overhead Multipliers (% of base coding hours)
# ============================================================
OVERHEAD_MULTIPLIERS = {
"architecture_design": {"low": 0.12, "high": 0.15, "label": "Architecture & Design"},
"debugging_troubleshooting": {"low": 0.20, "high": 0.25, "label": "Debugging & Troubleshooting"},
"code_review_refactoring": {"low": 0.08, "high": 0.12, "label": "Code Review & Refactoring"},
"documentation": {"low": 0.05, "high": 0.08, "label": "Documentation"},
"integration_testing": {"low": 0.15, "high": 0.18, "label": "Integration & Testing"},
"learning_curve": {"low": 0.08, "high": 0.15, "label": "Learning Curve"},
}
# ============================================================
# CONFIGURATION: Hourly Market Rates by Role (USD)
# ============================================================
ROLE_RATES = {
"engineering": {"low": 100, "mid": 150, "high": 225},
"product_mgmt": {"low": 125, "mid": 160, "high": 200},
"ux_design": {"low": 100, "mid": 140, "high": 175},
"eng_mgmt": {"low": 150, "mid": 185, "high": 225},
"qa_testing": {"low": 75, "mid": 100, "high": 125},
"project_mgmt": {"low": 100, "mid": 125, "high": 150},
"tech_writing": {"low": 75, "mid": 100, "high": 125},
"devops": {"low": 125, "mid": 160, "high": 200},
}
# ============================================================
# CONFIGURATION: Role Ratios by Company Stage
# ============================================================
ROLE_RATIOS = {
"solo": {
"product_mgmt": 0.00, "ux_design": 0.00, "eng_mgmt": 0.00,
"qa_testing": 0.00, "project_mgmt": 0.00, "tech_writing": 0.00, "devops": 0.00,
"multiplier": 1.0,
},
"lean_startup": {
"product_mgmt": 0.15, "ux_design": 0.15, "eng_mgmt": 0.05,
"qa_testing": 0.05, "project_mgmt": 0.00, "tech_writing": 0.00, "devops": 0.05,
"multiplier": 1.45,
},
"growth_company": {
"product_mgmt": 0.30, "ux_design": 0.25, "eng_mgmt": 0.15,
"qa_testing": 0.20, "project_mgmt": 0.10, "tech_writing": 0.05, "devops": 0.15,
"multiplier": 2.2,
},
"enterprise": {
"product_mgmt": 0.40, "ux_design": 0.35, "eng_mgmt": 0.20,
"qa_testing": 0.25, "project_mgmt": 0.15, "tech_writing": 0.10, "devops": 0.20,
"multiplier": 2.65,
},
}
# ============================================================
# CONFIGURATION: Organizational Efficiency
# ============================================================
ORG_EFFICIENCY = {
"solo": {"efficiency": 0.65, "hrs_per_week": 26, "label": "Solo/Startup (lean)"},
"growth": {"efficiency": 0.55, "hrs_per_week": 22, "label": "Growth Company"},
"enterprise": {"efficiency": 0.45, "hrs_per_week": 18, "label": "Enterprise"},
"bureaucracy": {"efficiency": 0.35, "hrs_per_week": 14, "label": "Large Bureaucracy"},
}
# Sanity check bounds
SANITY_LOW = 12
SANITY_HIGH = 40
SANITY_TARGET_LOW = 15
SANITY_TARGET_HIGH = 30
# Claude ROI constants
CLAUDE_CODING_SPEED_MID = 350 # lines/hr
HUMAN_BASELINE_RATE = 150 # $/hr
CLAUDE_SUBSCRIPTION_RANGE = (20, 200) # $/month
def midpoint(low: float, high: float) -> float:
return (low + high) / 2
def calculate(categories: dict[str, int], hourly_rate: float = 150.0,
claude_hours: float | None = None) -> dict:
"""Calculate full cost estimate from categorized line counts."""
# Step 1: Base coding hours
base_rows = []
total_lines = 0
total_base_hours = 0.0
for cat_key, lines in sorted(categories.items(), key=lambda x: x[1], reverse=True):
if lines <= 0:
continue
if cat_key not in RATE_TABLE:
print(f"Warning: unknown category '{cat_key}', using business_logic rates",
file=sys.stderr)
rate_info = RATE_TABLE["business_logic"]
else:
rate_info = RATE_TABLE[cat_key]
rate = midpoint(rate_info["low"], rate_info["high"])
hours = lines / rate
base_rows.append({
"category": rate_info.get("label", cat_key),
"lines": lines,
"rate": rate,
"hours": round(hours, 1),
})
total_lines += lines
total_base_hours += hours
# Step 2: Overhead
total_overhead_pct = 0.0
overhead_rows = []
for key, mult in OVERHEAD_MULTIPLIERS.items():
pct = midpoint(mult["low"], mult["high"])
hours = total_base_hours * pct
overhead_rows.append({
"category": mult["label"],
"percentage": round(pct * 100, 1),
"hours": round(hours, 1),
})
total_overhead_pct += pct
total_overhead_hours = total_base_hours * total_overhead_pct
total_hours = total_base_hours + total_overhead_hours
# Step 3: Sanity check
effective_lph = total_lines / total_hours if total_hours > 0 else 0
sanity_pass = SANITY_TARGET_LOW <= effective_lph <= SANITY_TARGET_HIGH
sanity_status = "PASS" if sanity_pass else "ADJUST"
# Step 4: Calendar time
calendar = {}
for key, org in ORG_EFFICIENCY.items():
weeks = total_hours / org["hrs_per_week"]
months = weeks / 4.33
calendar[key] = {
"label": org["label"],
"efficiency": f"{int(org['efficiency'] * 100)}%",
"hrs_per_week": org["hrs_per_week"],
"weeks": round(weeks, 1),
"months": round(months, 0),
}
# Step 5: Cost estimates
eng_cost_low = total_hours * ROLE_RATES["engineering"]["low"]
eng_cost_mid = total_hours * hourly_rate
eng_cost_high = total_hours * ROLE_RATES["engineering"]["high"]
# Step 6: Full team cost
team_costs = {}
for stage_key, ratios in ROLE_RATIOS.items():
eng_hours = total_hours
roles = [{"role": "Engineering", "hours": round(eng_hours, 0),
"rate": hourly_rate, "cost": round(eng_hours * hourly_rate, 0)}]
total_team_hours = eng_hours
total_team_cost = eng_hours * hourly_rate
for role_key, ratio in ratios.items():
if role_key == "multiplier" or ratio == 0:
continue
role_hours = eng_hours * ratio
role_rate = ROLE_RATES[role_key]["mid"]
role_cost = role_hours * role_rate
total_team_hours += role_hours
total_team_cost += role_cost
role_label = {
"product_mgmt": "Product Management",
"ux_design": "UX/UI Design",
"eng_mgmt": "Engineering Management",
"qa_testing": "QA/Testing",
"project_mgmt": "Project Management",
"tech_writing": "Technical Writing",
"devops": "DevOps/Platform",
}.get(role_key, role_key)
roles.append({
"role": role_label,
"hours": round(role_hours, 0),
"rate": role_rate,
"cost": round(role_cost, 0),
"ratio": f"{int(ratio * 100)}%",
})
team_costs[stage_key] = {
"multiplier": ratios["multiplier"],
"total_hours": round(total_team_hours, 0),
"total_cost": round(total_team_cost, 0),
"roles": roles,
}
# Step 7: Claude ROI (if claude_hours provided)
roi = None
if claude_hours and claude_hours > 0:
speed_multiplier = total_hours / claude_hours
human_cost = total_hours * HUMAN_BASELINE_RATE
# Estimate Claude cost: ~$200/month for duration
claude_months = max(1, claude_hours / (26 * 4.33)) # rough months
claude_cost = claude_months * 200 + (total_lines / 1000) * 0.5 # sub + API estimate
claude_cost = max(claude_cost, 200) # minimum 1 month
savings = human_cost - claude_cost
roi_multiplier = savings / claude_cost if claude_cost > 0 else 0
roi = {
"claude_hours": claude_hours,
"speed_multiplier": round(speed_multiplier, 0),
"human_hours": round(total_hours, 0),
"human_cost": round(human_cost, 0),
"claude_cost": round(claude_cost, 0),
"savings": round(savings, 0),
"roi_multiplier": round(roi_multiplier, 0),
"value_per_claude_hour": {
"engineering_avg": round(eng_cost_mid / claude_hours, 0),
"growth_company": round(team_costs["growth_company"]["total_cost"] / claude_hours, 0),
"enterprise": round(team_costs["enterprise"]["total_cost"] / claude_hours, 0),
},
}
return {
"base_coding": {
"rows": base_rows,
"total_lines": total_lines,
"total_hours": round(total_base_hours, 1),
},
"overhead": {
"rows": overhead_rows,
"total_percentage": round(total_overhead_pct * 100, 1),
"total_hours": round(total_overhead_hours, 1),
},
"total_estimated_hours": round(total_hours, 0),
"sanity_check": {
"effective_lines_per_hour": round(effective_lph, 1),
"target_range": f"{SANITY_TARGET_LOW}-{SANITY_TARGET_HIGH}",
"status": sanity_status,
},
"calendar_time": calendar,
"engineering_cost": {
"low": {"rate": ROLE_RATES["engineering"]["low"], "cost": round(eng_cost_low, 0)},
"mid": {"rate": hourly_rate, "cost": round(eng_cost_mid, 0)},
"high": {"rate": ROLE_RATES["engineering"]["high"], "cost": round(eng_cost_high, 0)},
},
"team_costs": team_costs,
"claude_roi": roi,
}
def main():
parser = argparse.ArgumentParser(description="Calculate development cost from categorized LOC")
parser.add_argument("--input", help="JSON file with {category: lines} mapping")
parser.add_argument("--rate", type=float, default=150.0,
help="Recommended hourly rate (default: $150)")
parser.add_argument("--claude-hours", type=float,
help="Estimated Claude active hours for ROI calculation")
args = parser.parse_args()
if args.input:
with open(args.input) as f:
categories = json.load(f)
else:
categories = json.loads(sys.stdin.read())
result = calculate(categories, args.rate, args.claude_hours)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Git Session Analyzer — Estimate development sessions and active hours from git history.
Part of the cost-estimate agent toolchain. Clusters commits into sessions using
a configurable time gap, then estimates active hours per session based on commit density.
Usage:
python3 git_session_analyzer.py # All commits, current branch
python3 git_session_analyzer.py --branch feat/foo # Specific branch
python3 git_session_analyzer.py --commit abc1234 # Single commit
python3 git_session_analyzer.py --gap-hours 6 # Custom session gap (default: 4h)
Session Duration Heuristics (from cost-estimate config):
1-2 commits in a session -> ~1 hour
3-5 commits -> ~2 hours
6-10 commits -> ~3 hours
10+ commits -> ~4 hours
NOTE: These are starting estimates. The agent should review and adjust upward
for large-scope commits (e.g. a single commit adding 5000+ lines likely took
2-4 hours, not 1 hour). Use `git show <hash> --stat` to check scope.
Session Clustering:
Commits within a 4-hour window are grouped into one session.
A gap of >4 hours between consecutive commits starts a new session.
Output JSON structure:
{
"scope": "full" | "branch:<name>" | "commit:<hash>",
"first_commit": "2026-02-22T06:21:33+04:00",
"last_commit": "2026-03-07T11:59:59+04:00",
"total_calendar_days": int,
"total_commits": int,
"total_sessions": int,
"estimated_active_hours": float, # Sum of all session estimates
"sessions": [
{
"date": "2026-02-22",
"start": "06:21",
"end": "07:24",
"commits": int,
"estimated_hours": float,
"subjects": ["commit msg 1", "commit msg 2", ...]
}
]
}
"""
import argparse
import json
import subprocess
import sys
from datetime import datetime, timedelta
def run(cmd: list[str]) -> str:
result = subprocess.run(cmd, capture_output=True, text=True)
return result.stdout.strip()
def get_commits(branch: str | None = None) -> list[dict]:
"""Get all commits with timestamps and subjects."""
cmd = ["git", "log", "--format=%H|%ai|%s"]
if branch:
cmd.append(branch)
output = run(cmd)
if not output:
return []
commits = []
for line in output.splitlines():
parts = line.split("|", 2)
if len(parts) == 3:
hash_val, timestamp_str, subject = parts
# Parse git date format: "2026-02-22 06:21:33 +0400"
try:
dt = datetime.strptime(timestamp_str.strip(), "%Y-%m-%d %H:%M:%S %z")
except ValueError:
continue
commits.append({
"hash": hash_val[:8],
"timestamp": dt,
"subject": subject.strip(),
})
# Sort chronologically
commits.sort(key=lambda c: c["timestamp"])
return commits
def cluster_into_sessions(commits: list[dict], gap_hours: float = 4.0) -> list[dict]:
"""Group commits into sessions based on time gaps."""
if not commits:
return []
sessions = []
current_session = [commits[0]]
for i in range(1, len(commits)):
gap = (commits[i]["timestamp"] - commits[i - 1]["timestamp"]).total_seconds() / 3600
if gap > gap_hours:
sessions.append(current_session)
current_session = [commits[i]]
else:
current_session.append(commits[i])
sessions.append(current_session)
return sessions
def estimate_session_hours(commit_count: int) -> float:
"""Estimate active hours from commit density.
Heuristics from cost-estimate agent config:
- 1-2 commits -> ~1 hour
- 3-5 commits -> ~2 hours
- 6-10 commits -> ~3 hours
- 10+ commits -> ~4 hours
"""
if commit_count <= 2:
return 1.0
elif commit_count <= 5:
return 2.0
elif commit_count <= 10:
return 3.0
else:
return 4.0
def main():
parser = argparse.ArgumentParser(description="Analyze git history for development sessions")
parser.add_argument("--branch", help="Analyze specific branch")
parser.add_argument("--commit", help="Analyze single commit")
parser.add_argument("--gap-hours", type=float, default=4.0,
help="Hours between commits to start new session (default: 4)")
args = parser.parse_args()
if args.commit:
# Single commit mode
output = run(["git", "show", args.commit, "--format=%H|%ai|%s", "--no-patch"])
parts = output.split("|", 2)
if len(parts) != 3:
print(json.dumps({"error": f"Could not parse commit {args.commit}"}))
sys.exit(1)
dt = datetime.strptime(parts[1].strip(), "%Y-%m-%d %H:%M:%S %z")
result = {
"scope": f"commit:{args.commit}",
"first_commit": dt.isoformat(),
"last_commit": dt.isoformat(),
"total_calendar_days": 1,
"total_commits": 1,
"total_sessions": 1,
"estimated_active_hours": 1.0,
"sessions": [{
"date": dt.strftime("%Y-%m-%d"),
"start": dt.strftime("%H:%M"),
"end": dt.strftime("%H:%M"),
"commits": 1,
"estimated_hours": 1.0,
"subjects": [parts[2].strip()],
}],
}
print(json.dumps(result, indent=2))
return
commits = get_commits(args.branch)
if not commits:
print(json.dumps({"error": "No commits found"}))
sys.exit(1)
sessions = cluster_into_sessions(commits, args.gap_hours)
first_commit = commits[0]["timestamp"]
last_commit = commits[-1]["timestamp"]
calendar_days = max(1, (last_commit - first_commit).days + 1)
session_details = []
total_active_hours = 0.0
for session_commits in sessions:
start = session_commits[0]["timestamp"]
end = session_commits[-1]["timestamp"]
count = len(session_commits)
hours = estimate_session_hours(count)
total_active_hours += hours
subjects = [c["subject"] for c in session_commits]
session_details.append({
"date": start.strftime("%Y-%m-%d"),
"start": start.strftime("%H:%M"),
"end": end.strftime("%H:%M"),
"commits": count,
"estimated_hours": hours,
"subjects": subjects,
})
result = {
"scope": f"branch:{args.branch}" if args.branch else "full",
"first_commit": first_commit.isoformat(),
"last_commit": last_commit.isoformat(),
"total_calendar_days": calendar_days,
"total_commits": len(commits),
"total_sessions": len(sessions),
"estimated_active_hours": total_active_hours,
"sessions": session_details,
}
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
LOC Counter — Count lines of code in any git repository.
Part of the cost-estimate agent toolchain. Respects .gitignore via `git ls-files`.
Outputs structured JSON for downstream classification and cost calculation.
Usage:
python3 loc_counter.py # Full repo
python3 loc_counter.py --branch feat/foo # Branch diff (added lines only)
python3 loc_counter.py --branch feat/foo --base develop # Diff against specific base
python3 loc_counter.py --commit abc1234 # Single commit diff
python3 loc_counter.py --exclude "vendor/*" # Exclude patterns
Output JSON structure:
{
"scope": "full" | "branch:<name>" | "commit:<hash>",
"totals": {
"lines": int, # Total lines counted
"files": int, # Total files counted
"source_lines": int, # Lines excluding tests, docs, config
"test_lines": int, # Lines in test files
"doc_lines": int, # Lines in documentation files
"config_lines": int # Lines in config/build files
},
"by_language": { # Grouped by detected language
"Swift": {"lines": int, "files": int},
...
},
"by_directory": { # Grouped by top-level directory
"src/components": {"lines": int, "files": int},
...
},
"top_files": [...], # Top 50 files by line count
"all_files": [ # Every file with metadata
{
"path": str,
"lines": int,
"category": str, # Detected language/type
"directory": str,
"is_test": bool,
"is_doc": bool,
"is_config": bool
}
]
}
File classification heuristics:
- Tests: files in tests/test/__tests__/spec dirs, or *_test.*, *.test.*, *.spec.*
- Docs: .md, .rst, .adoc, .txt files
- Config: .plist, .entitlements, .yml in config dirs, package.json, Makefile, etc.
- Binary files and common non-source extensions (.png, .mp3, .zip, etc.) are skipped
"""
import argparse
import json
import os
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
def run(cmd: list[str], cwd: str | None = None) -> str:
result = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd)
return result.stdout.strip()
def get_git_root() -> str:
root = run(["git", "rev-parse", "--show-toplevel"])
if not root:
print("Error: not a git repository", file=sys.stderr)
sys.exit(1)
return root
def get_tracked_files(git_root: str) -> list[str]:
output = run(["git", "ls-files"], cwd=git_root)
return [f for f in output.splitlines() if f]
def count_lines(filepath: str) -> int:
try:
with open(filepath, "r", errors="replace") as f:
return sum(1 for _ in f)
except (OSError, UnicodeDecodeError):
return 0
def is_binary(filepath: str) -> bool:
try:
with open(filepath, "rb") as f:
chunk = f.read(8192)
return b"\x00" in chunk
except OSError:
return True
def get_branch_diff_files(branch: str, base: str, git_root: str) -> dict[str, int]:
"""Get added lines per file for a branch diff."""
output = run(
["git", "diff", f"{base}...{branch}", "--numstat"], cwd=git_root
)
result = {}
for line in output.splitlines():
parts = line.split("\t")
if len(parts) == 3:
added, _, filepath = parts
if added != "-": # skip binary
result[filepath] = int(added)
return result
def get_commit_diff_files(commit: str, git_root: str) -> dict[str, int]:
"""Get added lines per file for a single commit."""
output = run(["git", "show", commit, "--numstat", "--format="], cwd=git_root)
result = {}
for line in output.splitlines():
parts = line.split("\t")
if len(parts) == 3:
added, _, filepath = parts
if added != "-":
result[filepath] = int(added)
return result
# Common non-source extensions to skip
SKIP_EXTENSIONS = {
".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".webp", ".bmp", ".tiff",
".mp3", ".mp4", ".wav", ".m4a", ".aac", ".ogg", ".mov", ".avi",
".zip", ".tar", ".gz", ".bz2", ".xz", ".rar", ".7z",
".woff", ".woff2", ".ttf", ".otf", ".eot",
".pdf", ".doc", ".docx", ".xls", ".xlsx",
".pyc", ".pyo", ".class", ".o", ".a", ".dylib", ".so", ".dll",
".DS_Store", ".lock",
".pbxproj", # Xcode project files (generated)
}
# File extensions grouped by category
EXTENSION_CATEGORIES = {
# Languages
".swift": "Swift",
".m": "Objective-C",
".mm": "Objective-C++",
".h": "C/ObjC Header",
".c": "C",
".cpp": "C++",
".cc": "C++",
".cxx": "C++",
".rs": "Rust",
".go": "Go",
".java": "Java",
".kt": "Kotlin",
".kts": "Kotlin",
".scala": "Scala",
".py": "Python",
".rb": "Ruby",
".php": "PHP",
".js": "JavaScript",
".jsx": "JavaScript (JSX)",
".ts": "TypeScript",
".tsx": "TypeScript (TSX)",
".vue": "Vue",
".svelte": "Svelte",
".dart": "Dart",
".lua": "Lua",
".r": "R",
".ex": "Elixir",
".exs": "Elixir",
".erl": "Erlang",
".hs": "Haskell",
".ml": "OCaml",
".cs": "C#",
".fs": "F#",
".clj": "Clojure",
".zig": "Zig",
".nim": "Nim",
".metal": "Metal (GPU)",
".glsl": "GLSL (GPU)",
".hlsl": "HLSL (GPU)",
".wgsl": "WGSL (GPU)",
".cu": "CUDA",
# Markup & styles
".html": "HTML",
".htm": "HTML",
".css": "CSS",
".scss": "SCSS",
".sass": "Sass",
".less": "Less",
# Data & config
".json": "JSON",
".yaml": "YAML",
".yml": "YAML",
".toml": "TOML",
".xml": "XML",
".plist": "Property List",
".entitlements": "Entitlements",
".xcconfig": "Xcode Config",
".storyboard": "Storyboard",
".xib": "XIB",
# Build & CI
".gradle": "Gradle",
".cmake": "CMake",
".makefile": "Makefile",
".dockerfile": "Dockerfile",
".tf": "Terraform",
".hcl": "HCL",
# Docs
".md": "Markdown",
".rst": "reStructuredText",
".txt": "Text",
".adoc": "AsciiDoc",
# Shell
".sh": "Shell",
".bash": "Shell",
".zsh": "Shell",
".fish": "Fish",
".ps1": "PowerShell",
# SQL
".sql": "SQL",
}
def categorize_file(filepath: str) -> str:
"""Determine category from file extension."""
name = os.path.basename(filepath).lower()
# Special filenames
if name in ("makefile", "gnumakefile"):
return "Makefile"
if name in ("dockerfile", "containerfile"):
return "Dockerfile"
if name in ("package.swift",):
return "Swift Package Manifest"
if name in ("project.yml", "project.yaml"):
return "XcodeGen Config"
if name in ("podfile",):
return "CocoaPods"
if name in ("gemfile",):
return "Ruby (Gemfile)"
if name in ("cargo.toml",):
return "Cargo Config"
ext = Path(filepath).suffix.lower()
return EXTENSION_CATEGORIES.get(ext, f"Other ({ext})" if ext else "Other")
def should_skip(filepath: str, exclude_patterns: list[str]) -> bool:
ext = Path(filepath).suffix.lower()
if ext in SKIP_EXTENSIONS:
return True
name = os.path.basename(filepath)
if name in (".DS_Store", "Thumbs.db"):
return True
for pattern in exclude_patterns:
if pattern.startswith("*.") and filepath.endswith(pattern[1:]):
return True
if "/" in pattern and pattern.rstrip("*") in filepath:
return True
return False
def detect_test_file(filepath: str) -> bool:
parts = filepath.lower().split("/")
name = os.path.basename(filepath).lower()
# Directory-based
if any(p in ("tests", "test", "__tests__", "spec", "specs", "testing") for p in parts):
return True
# Name-based
if any(name.endswith(s) for s in ("test.swift", "tests.swift", "_test.go",
"_test.py", ".test.ts", ".test.js",
".spec.ts", ".spec.js", "_spec.rb")):
return True
if name.startswith("test_") or name.startswith("test."):
return True
return False
def detect_doc_file(filepath: str) -> bool:
ext = Path(filepath).suffix.lower()
if ext in (".md", ".rst", ".adoc", ".txt"):
name = os.path.basename(filepath).lower()
# Docs but not changelogs or licenses which are boilerplate
return True
return False
def detect_config_file(filepath: str) -> bool:
ext = Path(filepath).suffix.lower()
name = os.path.basename(filepath).lower()
config_exts = {".plist", ".entitlements", ".xcconfig", ".yml", ".yaml",
".toml", ".json", ".xml"}
config_names = {"package.swift", "project.yml", "project.yaml",
"podfile", "gemfile", "cargo.toml", "tsconfig.json",
"package.json", ".eslintrc", ".prettierrc",
"dockerfile", "docker-compose.yml", ".env.example",
"makefile", ".gitignore", ".editorconfig"}
if name in config_names:
return True
if ext in config_exts:
# Only if it looks like a config, not app data
parts = filepath.lower().split("/")
if any(p in ("config", "configs", "configuration", ".github", ".circleci") for p in parts):
return True
if name.endswith("config.json") or name.endswith("rc.json"):
return True
return False
def main():
parser = argparse.ArgumentParser(description="Count lines of code in a repository")
parser.add_argument("--branch", help="Count only added lines in branch diff")
parser.add_argument("--base", default="main", help="Base branch for diff (default: main)")
parser.add_argument("--commit", help="Count only added lines in a single commit")
parser.add_argument("--exclude", action="append", default=[], help="Glob patterns to exclude")
parser.add_argument("--top-dirs", type=int, default=3, help="Depth for directory grouping")
args = parser.parse_args()
git_root = get_git_root()
scope = "full"
diff_lines: dict[str, int] = {}
if args.branch:
scope = f"branch:{args.branch}"
diff_lines = get_branch_diff_files(args.branch, args.base, git_root)
elif args.commit:
scope = f"commit:{args.commit}"
diff_lines = get_commit_diff_files(args.commit, git_root)
files = get_tracked_files(git_root)
# Per-directory stats
dir_stats: dict[str, dict] = defaultdict(lambda: {"lines": 0, "files": 0, "extensions": defaultdict(int)})
# Per-extension stats
ext_stats: dict[str, dict] = defaultdict(lambda: {"lines": 0, "files": 0})
# Per-file detail
file_details: list[dict] = []
# Totals
total_lines = 0
total_files = 0
test_lines = 0
test_files = 0
doc_lines = 0
doc_files = 0
config_lines = 0
config_files = 0
for rel_path in files:
if should_skip(rel_path, args.exclude):
continue
full_path = os.path.join(git_root, rel_path)
if scope == "full":
if not os.path.isfile(full_path):
continue
if is_binary(full_path):
continue
lines = count_lines(full_path)
else:
if rel_path not in diff_lines:
continue
lines = diff_lines[rel_path]
if lines == 0:
continue
category = categorize_file(rel_path)
is_test = detect_test_file(rel_path)
is_doc = detect_doc_file(rel_path)
is_config = detect_config_file(rel_path)
# Directory grouping (top N levels)
parts = rel_path.split("/")
dir_key = "/".join(parts[:min(args.top_dirs, len(parts) - 1)]) or "."
dir_stats[dir_key]["lines"] += lines
dir_stats[dir_key]["files"] += 1
ext = Path(rel_path).suffix.lower() or os.path.basename(rel_path)
dir_stats[dir_key]["extensions"][ext] += lines
ext_stats[category]["lines"] += lines
ext_stats[category]["files"] += 1
total_lines += lines
total_files += 1
if is_test:
test_lines += lines
test_files += 1
if is_doc:
doc_lines += lines
doc_files += 1
if is_config:
config_lines += lines
config_files += 1
file_details.append({
"path": rel_path,
"lines": lines,
"category": category,
"directory": dir_key,
"is_test": is_test,
"is_doc": is_doc,
"is_config": is_config,
})
# Sort file details by lines descending
file_details.sort(key=lambda x: x["lines"], reverse=True)
# Build output
output = {
"scope": scope,
"git_root": git_root,
"totals": {
"lines": total_lines,
"files": total_files,
"test_lines": test_lines,
"test_files": test_files,
"doc_lines": doc_lines,
"doc_files": doc_files,
"config_lines": config_lines,
"config_files": config_files,
"source_lines": total_lines - test_lines - doc_lines - config_lines,
},
"by_language": {
k: v for k, v in sorted(ext_stats.items(), key=lambda x: x[1]["lines"], reverse=True)
},
"by_directory": {
k: {"lines": v["lines"], "files": v["files"]}
for k, v in sorted(dir_stats.items(), key=lambda x: x[1]["lines"], reverse=True)
},
"top_files": file_details[:50],
"all_files": file_details,
}
print(json.dumps(output, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Report Generator — Format cost estimate data into markdown report sections.
Part of the cost-estimate agent toolchain. Takes the JSON output from
cost_calculator.py (and optionally git_session_analyzer.py) and generates
ready-to-paste markdown sections for the final COST-ESTIMATE.md report.
Can generate individual sections or the full report. The agent can then
review, adjust descriptions, and add complexity factors and market research.
Usage:
# Generate full report from calculator output
python3 report_generator.py --calc costs.json --sessions sessions.json --project "MyApp"
# Generate specific section only
python3 report_generator.py --calc costs.json --section executive_summary
python3 report_generator.py --calc costs.json --section development_time
python3 report_generator.py --calc costs.json --section calendar_time
python3 report_generator.py --calc costs.json --section engineering_cost
python3 report_generator.py --calc costs.json --section team_cost
python3 report_generator.py --calc costs.json --section grand_total
python3 report_generator.py --calc costs.json --sessions sessions.json --section claude_roi
# Pipe calculator output directly
echo '{"audio_video_processing": 2000}' | python3 cost_calculator.py --rate 150 --claude-hours 29 | python3 report_generator.py --project "MyApp"
Available sections:
executive_summary - Key numbers table + Claude ROI headline (goes at top of report)
development_time - Code category table + overhead multipliers + sanity check
calendar_time - Organizational efficiency table
engineering_cost - Low/avg/high engineering-only cost table
team_cost - Full team cost by company stage + role breakdown
grand_total - Summary table across all company stages
claude_roi - Project timeline, sessions, value per hour, speed/cost comparison
assumptions - Standard assumptions section
Output: Markdown text printed to stdout. Redirect to file or copy into report.
"""
import argparse
import json
import sys
def fmt(n: float, prefix: str = r"\$") -> str:
"""Format number with commas and optional prefix. Default prefix is escaped $ for markdown."""
return f"{prefix}{n:,.0f}"
def months_label(months: float) -> str:
if months < 12:
return f"~{int(months)} months"
years = months / 12
if years == int(years):
return f"~{int(years)} years"
return f"~{years:.1f} years"
def section_executive_summary(calc: dict, sessions: dict | None, project: str) -> str:
total_lines = calc["base_coding"]["total_lines"]
total_hours = calc["total_estimated_hours"]
eng_cost = calc["engineering_cost"]["mid"]["cost"]
growth_cost = calc["team_costs"]["growth_company"]["total_cost"]
solo_months = calc["calendar_time"]["solo"]["months"]
lines = [
f"## Executive Summary",
"",
f"| Metric | Value |",
f"|--------|-------|",
f"| **Total Lines of Code** | {total_lines:,} |",
f"| **Estimated Engineering Hours** | {total_hours:,} |",
f"| **Engineering Cost (avg rate)** | {fmt(eng_cost)} |",
f"| **Full Team Cost (Growth Co)** | {fmt(growth_cost)} |",
f"| **Calendar Time (solo, lean)** | {months_label(solo_months)} |",
"",
]
roi = calc.get("claude_roi")
if roi and sessions:
claude_hrs = roi["claude_hours"]
speed = roi["speed_multiplier"]
val_per_hr = roi["value_per_claude_hour"]["engineering_avg"]
roi_mult = roi["roi_multiplier"]
claude_cost = roi["claude_cost"]
days = sessions.get("total_calendar_days", "?")
lines.extend([
"### Claude ROI",
"",
"| Metric | Value |",
"|--------|-------|",
f"| **Claude active hours** | ~{int(claude_hrs)} hours (across {days} calendar days) |",
f"| **Speed multiplier** | {int(speed)}x faster than human developer |",
f"| **Value per Claude hour** | {fmt(val_per_hr)}/hr (engineering) |",
f"| **ROI** | {int(roi_mult)}x ({fmt(eng_cost / 1000)}k value for ~{fmt(claude_cost)} in Claude costs) |",
"",
f"> Claude worked ~{int(claude_hrs)} hours and produced {fmt(eng_cost)} of professional development value = **{fmt(val_per_hr)} per Claude hour**",
"",
])
return "\n".join(lines)
def section_development_time(calc: dict) -> str:
lines = [
"## Development Time Estimate",
"",
"### Code Category Classification",
"",
"| Code Category | Lines | Rate (lines/hr) | Hours |",
"|---------------|-------|-----------------|-------|",
]
for row in calc["base_coding"]["rows"]:
lines.append(
f"| {row['category']} | {row['lines']:,} | {row['rate']:.0f} | {row['hours']:.1f} |"
)
lines.append(
f"| **TOTAL** | **{calc['base_coding']['total_lines']:,}** | | **{calc['base_coding']['total_hours']:.0f}** |"
)
lines.extend([
"",
f"### Base Development Hours: {calc['base_coding']['total_hours']:.0f} hours",
"",
"### Overhead Multipliers",
"",
"| Overhead Category | Rate (midpoint) | Hours |",
"|-------------------|-----------------|-------|",
])
for row in calc["overhead"]["rows"]:
lines.append(f"| {row['category']} | {row['percentage']}% | {row['hours']:.1f} |")
lines.append(
f"| **Total Overhead** | **{calc['overhead']['total_percentage']}%** | **{calc['overhead']['total_hours']:.1f}** |"
)
sc = calc["sanity_check"]
lines.extend([
"",
f"### Total Estimated Hours: {calc['total_estimated_hours']:,} hours",
"",
"### Sanity Check",
"",
f"{calc['base_coding']['total_lines']:,} LOC / {calc['total_estimated_hours']:,} total hours = **{sc['effective_lines_per_hour']} effective lines/hour**",
"",
f"**{sc['status']}** -- {'within' if sc['status'] == 'PASS' else 'outside'} target range of {sc['target_range']} lines/hour.",
"",
])
return "\n".join(lines)
def section_calendar_time(calc: dict) -> str:
lines = [
"## Realistic Calendar Time (with Organizational Overhead)",
"",
"| Company Type | Efficiency | Coding Hrs/Week | Calendar Weeks | Calendar Time |",
"|--------------|------------|-----------------|----------------|---------------|",
]
for key in ["solo", "growth", "enterprise", "bureaucracy"]:
ct = calc["calendar_time"][key]
lines.append(
f"| {ct['label']} | {ct['efficiency']} | {ct['hrs_per_week']} hrs | {ct['weeks']:.1f} weeks | {months_label(ct['months'])} |"
)
lines.extend([
"",
"*Note: These represent single-developer calendar time. With a 2-3 person team, divide by team size (with ~80% scaling efficiency).*",
"",
])
return "\n".join(lines)
def section_engineering_cost(calc: dict) -> str:
ec = calc["engineering_cost"]
hours = calc["total_estimated_hours"]
lines = [
"## Total Cost Estimate (Engineering Only)",
"",
"| Scenario | Hourly Rate | Total Hours | **Total Cost** |",
"|----------|-------------|-------------|----------------|",
f"| Low-end | {fmt(ec['low']['rate'])}/hr | {hours:,} | **{fmt(ec['low']['cost'])}** |",
f"| Average | {fmt(ec['mid']['rate'])}/hr | {hours:,} | **{fmt(ec['mid']['cost'])}** |",
f"| High-end | {fmt(ec['high']['rate'])}/hr | {hours:,} | **{fmt(ec['high']['cost'])}** |",
"",
f"**Recommended Estimate (Engineering Only)**: **{fmt(ec['low']['cost'])} - {fmt(ec['mid']['cost'])}**",
"",
]
return "\n".join(lines)
def section_team_cost(calc: dict) -> str:
tc = calc["team_costs"]
eng_cost = calc["engineering_cost"]["mid"]["cost"]
lines = [
"## Full Team Cost (All Roles)",
"",
"| Company Stage | Team Multiplier | Engineering Cost (avg) | **Full Team Cost** |",
"|---------------|-----------------|------------------------|--------------------|",
]
stage_labels = {
"solo": "Solo/Founder",
"lean_startup": "Lean Startup",
"growth_company": "Growth Company",
"enterprise": "Enterprise",
}
for key in ["solo", "lean_startup", "growth_company", "enterprise"]:
stage = tc[key]
lines.append(
f"| {stage_labels[key]} | {stage['multiplier']}x | {fmt(eng_cost)} | **{fmt(stage['total_cost'])}** |"
)
# Growth company role breakdown
gc = tc["growth_company"]
lines.extend([
"",
"### Role Breakdown (Growth Company Example)",
"",
"| Role | Hours | Rate | Cost |",
"|------|-------|------|------|",
])
for role in gc["roles"]:
ratio = f" ({role['ratio']})" if "ratio" in role else ""
lines.append(
f"| {role['role']}{ratio} | {int(role['hours']):,} hrs | {fmt(role['rate'])}/hr | {fmt(role['cost'])} |"
)
lines.append(
f"| **TOTAL** | **{int(gc['total_hours']):,} hrs** | | **{fmt(gc['total_cost'])}** |"
)
lines.append("")
return "\n".join(lines)
def section_grand_total(calc: dict) -> str:
tc = calc["team_costs"]
ct = calc["calendar_time"]
lines = [
"## Grand Total Summary",
"",
"| Metric | Solo | Lean Startup | Growth Co | Enterprise |",
"|--------|------|--------------|-----------|------------|",
f"| Calendar Time (1 dev) | {months_label(ct['solo']['months'])} | {months_label(ct['growth']['months'])} | {months_label(ct['enterprise']['months'])} | {months_label(ct['bureaucracy']['months'])} |",
f"| Total Human Hours | {int(tc['solo']['total_hours']):,} | {int(tc['lean_startup']['total_hours']):,} | {int(tc['growth_company']['total_hours']):,} | {int(tc['enterprise']['total_hours']):,} |",
f"| **Total Cost** | **{fmt(tc['solo']['total_cost'])}** | **{fmt(tc['lean_startup']['total_cost'])}** | **{fmt(tc['growth_company']['total_cost'])}** | **{fmt(tc['enterprise']['total_cost'])}** |",
"",
]
return "\n".join(lines)
def section_claude_roi(calc: dict, sessions: dict | None) -> str:
roi = calc.get("claude_roi")
if not roi:
return "## Claude ROI Analysis\n\n*No Claude hours provided. Pass --claude-hours to cost_calculator.py.*\n"
lines = [
"## Claude ROI Analysis (Detailed)",
"",
]
if sessions:
lines.extend([
"### Project Timeline",
"",
f"- **First commit**: {sessions['first_commit'][:10]}",
f"- **Latest commit**: {sessions['last_commit'][:10]}",
f"- **Total calendar time**: {sessions['total_calendar_days']} days (~{sessions['total_calendar_days'] / 7:.1f} weeks)",
"",
"### Claude Active Hours Estimate",
"",
f"Analyzing the git history ({sessions['total_commits']} commits), development sessions cluster as follows:",
"",
"| Date | Session Window | Commits | Est. Hours |",
"|------|---------------|---------|------------|",
])
for s in sessions["sessions"]:
summary = s["subjects"][0][:60] if s["subjects"] else ""
if len(s["subjects"]) > 1:
summary += f" (+{len(s['subjects']) - 1} more)"
lines.append(
f"| {s['date']} | {s['start']}-{s['end']} | {s['commits']} | {s['estimated_hours']:.0f} |"
)
lines.extend([
"",
f"- **Total sessions identified**: {sessions['total_sessions']} sessions",
f"- **Estimated active hours**: ~{int(roi['claude_hours'])} hours",
f"- **Method**: Git commit clustering (4-hour window grouping with density-based duration estimation)",
"",
])
vph = roi["value_per_claude_hour"]
lines.extend([
"### Value per Claude Hour",
"",
"| Value Basis | Total Value | Claude Hours | \\$/Claude Hour |",
"|-------------|-------------|--------------|---------------|",
f"| Engineering only (avg) | {fmt(calc['engineering_cost']['mid']['cost'])} | {int(roi['claude_hours'])} hrs | **{fmt(vph['engineering_avg'])}/Claude hr** |",
f"| Full team (Growth Co) | {fmt(calc['team_costs']['growth_company']['total_cost'])} | {int(roi['claude_hours'])} hrs | **{fmt(vph['growth_company'])}/Claude hr** |",
f"| Full team (Enterprise) | {fmt(calc['team_costs']['enterprise']['total_cost'])} | {int(roi['claude_hours'])} hrs | **{fmt(vph['enterprise'])}/Claude hr** |",
"",
"### Speed vs. Human Developer",
"",
f"- Estimated human hours for same work: **{int(roi['human_hours']):,} hours** (engineering only)",
f"- Claude active hours: **{int(roi['claude_hours'])} hours**",
f"- **Speed multiplier: {int(roi['speed_multiplier'])}x** (Claude produced code {int(roi['speed_multiplier'])}x faster than a human developer)",
"",
"### Cost Comparison",
"",
f"- Human developer cost: **{fmt(roi['human_cost'])}** ({int(roi['human_hours']):,} hrs at \\$150/hr baseline rate)",
f"- Estimated Claude cost: **~{fmt(roi['claude_cost'])}** (subscription + API)",
f"- **Net savings: {fmt(roi['savings'])}**",
f"- **ROI: {int(roi['roi_multiplier'])}x** (every \\$1 spent on Claude produced \\${int(roi['roi_multiplier'])} of value)",
"",
])
return "\n".join(lines)
def section_assumptions() -> str:
return """## Assumptions
1. Rates based on US market averages (2025-2026)
2. Full-time equivalent allocation for all roles
3. Lines of code counted include comments and blank lines (standard for LOC-based estimation)
4. Overhead multipliers capture architecture, debugging, code review, documentation, integration, and learning curve
5. Claude active hours estimated from git commit clustering -- actual wall-clock time may vary
6. Does not include:
- Marketing & sales
- Legal & compliance
- Office/equipment costs
- Hosting/infrastructure
- Ongoing maintenance post-launch
"""
SECTIONS = {
"executive_summary": lambda c, s, p: section_executive_summary(c, s, p),
"development_time": lambda c, s, p: section_development_time(c),
"calendar_time": lambda c, s, p: section_calendar_time(c),
"engineering_cost": lambda c, s, p: section_engineering_cost(c),
"team_cost": lambda c, s, p: section_team_cost(c),
"grand_total": lambda c, s, p: section_grand_total(c),
"claude_roi": lambda c, s, p: section_claude_roi(c, s),
"assumptions": lambda c, s, p: section_assumptions(),
}
FULL_REPORT_ORDER = [
"executive_summary",
"grand_total",
"development_time",
"calendar_time",
"engineering_cost",
"team_cost",
"claude_roi",
"assumptions",
]
def main():
parser = argparse.ArgumentParser(description="Generate markdown report sections from cost data")
parser.add_argument("--calc", help="JSON file from cost_calculator.py (or pipe via stdin)")
parser.add_argument("--sessions", help="JSON file from git_session_analyzer.py")
parser.add_argument("--project", default="Project", help="Project name for report title")
parser.add_argument("--section", choices=list(SECTIONS.keys()),
help="Generate only this section (default: full report)")
parser.add_argument("--scope", default="Full codebase",
help="Scope description (e.g. 'Branch feat/foo diff from main')")
args = parser.parse_args()
# Load calculator output
if args.calc:
with open(args.calc) as f:
calc = json.load(f)
else:
calc = json.loads(sys.stdin.read())
# Load sessions if provided
sessions = None
if args.sessions:
with open(args.sessions) as f:
sessions = json.load(f)
if args.section:
print(SECTIONS[args.section](calc, sessions, args.project))
else:
# Full report
header = f"""# {args.project} - Development Cost Estimate
**Analysis Date**: [Current Date]
**Scope**: {args.scope}
---
"""
print(header)
for section_key in FULL_REPORT_ORDER:
print(SECTIONS[section_key](calc, sessions, args.project))
print("---\n")
if __name__ == "__main__":
main()