Work Estimation skill for Claude Code
Ranges with confidence levels, not wishful dates. Claude reads the code that will change, breaks the feature into half-day to three-day tasks including review, rollout and docs, and turns big unknowns into spikes instead of padded guesses. Three-point estimates feed a bundled Monte Carlo script that reports P50, P80 and P90 totals plus the tasks driving the most uncertainty, then converts effort into calendar time with stated assumptions.
- name
work-estimation - size198 lines · ≈2,000 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Breaks features into small estimable tasks, surfaces unknowns and dependencies, estimates in three-point ranges instead of single numbers, and rolls them up with a Monte Carlo script into P50 and P80 forecasts with explicit risk buffers and assumptions. Use when the user asks how long something will take, wants a feature or project estimated, needs a work breakdown or sprint plan, must give a deadline or quote, or wants to re-forecast a slipping project. Try asking
- How long will annual billing plans take? Give me a range I can commit to
- Break this epic into tasks and estimate each one
- We're two weeks late — re-forecast the remaining work
Use it when
- Someone asks how long a feature or project will take
- A deadline, quote, or sprint plan needs a defensible number
- An epic needs breaking into estimable tasks
- A project is slipping and needs a re-forecast
Skip it when
- The main question is whether an approach works at all — use technical-spike first
- You need milestones and acceptance criteria for an agent to execute — use task-decomposition-plans
How to check the result
Every task sits between half a day and three days, the breakdown includes test, review and rollout work, and the quoted forecast comes from the simulation with its confidence level stated.
The common mistake it prevents
Summing the most-likely values of every task and presenting that as the deadline, which the simulation typically shows has well under a 20% chance of holding.
A good estimate is a range with a stated confidence, built from tasks small enough to reason about, with the unknowns named and the assumptions written down. “Two weeks” is a guess; “P50 8 days, P80 12 days, assuming the Stripe sandbox supports annual proration, which we verify on day 1” is an estimate someone can plan around.
Ground rules
- Never give a single number without a confidence level. Commit to P80 or higher for external deadlines; P50 is a coin flip.
- Estimate effort for the whole definition of done: code, tests, review rounds, docs, migration, deploy, and verification. Coding is often under half of it.
- Tasks larger than ~3 days of effort hide work; split them until each is 0.5–3 days.
- Unknowns are not tasks to estimate; they are questions to resolve. Convert big unknowns into time-boxed spikes (see technical-spike) before estimating what depends on them.
- Separate effort (person-days of work) from duration (calendar time). Duration includes meetings, reviews, waiting, on-call, and parallelism limits.
- Ground estimates in the codebase: read the code that will change before sizing it.
Workflow
- [ ] 1. Clarify scope and definition of done
- [ ] 2. Inspect the code and systems involved
- [ ] 3. Break down into tasks (0.5–3 days each)
- [ ] 4. Classify unknowns; spike the big ones
- [ ] 5. Three-point estimate each task
- [ ] 6. Roll up with Monte Carlo; add buffers
- [ ] 7. Convert effort to calendar duration
- [ ] 8. Present ranges, assumptions, and risks
1. Scope
Write in-scope and out-of-scope bullets, and the definition of done (e.g. “behind a flag in production, with monitoring, docs updated”). Ambiguous requirements get a listed assumption, not a silent interpretation.
2. Inspect
Read the modules that will change; check test coverage there, recent churn, and integration points. Look for hidden work: migrations on large tables, public API changes needing versioning, missing test infrastructure, permissions/compliance reviews, other teams’ dependencies.
3. Break down
Slice vertically where possible (each slice delivers something testable end to end) rather than by layer. Include the non-coding tasks explicitly:
- Data model + migration for plan intervals
- Proration calculation + unit tests
- Stripe price sync for annual plans
- Plan picker UI (monthly/annual toggle)
- Admin override for interval changes
- Feature flag, metrics, alert on proration errors
- Docs and support macro
- Staged rollout + verification in production
- Code review rounds and fixes (per PR, if not folded into tasks)
4. Classify unknowns
| Type | Example | Handling |
|---|---|---|
| Known knowns | Add a column, a CRUD endpoint | Estimate directly; narrow range |
| Known unknowns | “Does the vendor API support X?” | Spike (hours) first, or widen the range and add risk |
| Integration risk | Another team’s service or approval | Dependency with a date; track separately |
| Unknown unknowns | Unlisted work that always appears | Global buffer (10–25%) based on team history |
Ask the user for anything only they can answer (scope choices, deadlines, team size); do not estimate around missing decisions silently.
5. Three-point estimates
For each task give low (everything goes right, ~10% chance of beating it), likely
(most common outcome), and high (things go wrong but not catastrophically, ~90%
chance of beating it).
Heuristics:
- Ratio high/low of 2–3x for familiar work; 4x+ signals an unknown worth a spike.
- Anchor on reference classes: how long did similar past tasks take? Check
git log --sincedates for comparable features or past tickets. Past actuals beat intuition. - If you cannot name what would make a task take its
highvalue, you do not understand the task yet. - Add
risk(0–1) for tasks with a discrete chance of a surprise (vendor limitation, migration on a hot table).
PERT mean for a quick single task: (low + 4 x likely + high) / 6. Do not add P80s of
tasks together; that overstates the total. Simulate instead.
6. Roll up
Put tasks in a CSV and run the bundled simulator:
task,low,likely,high,risk
Data model + migration,1,1.5,3,
Proration logic + tests,2,3,6,0.2
Stripe price sync,1,2,5,0.3
Plan picker UI,1,2,3,
Flag + metrics + rollout,1,1.5,3,
python3 scripts/estimate.py tasks.csv --seed 7 --risk-buffer 0.15
It prints P50/P80/P90/P95 totals, the probability of hitting the naive sum of
likely values (usually low; that is the planning fallacy in one number), and which
tasks drive most of the uncertainty. Spike or split the top variance drivers first;
narrowing them shrinks the range more than anything else.
Buffer guidance (--risk-buffer): 0.10 for a stable team in a familiar codebase, 0.15–0.25
for new codebases, new tech, or cross-team work. If the team has historical
estimate-vs-actual data, use the observed ratio instead.
7. Effort to duration
duration (days) = effort (person-days) / (people x focus factor)
- Focus factor: 0.6–0.7 for typical teams (meetings, reviews, support), lower with on-call or interrupt-heavy roles.
- Parallelism is limited by dependencies; the critical path sets the minimum duration. Adding people late adds coordination cost.
- Add fixed waits: code review latency, QA, change-approval windows, app store review, vendor turnaround.
8. Present
## Estimate: Annual billing plans
**Forecast (effort):** P50 13 person-days, P80 15, P90 17 (includes 15% unlisted-work buffer)
**Calendar:** with 2 engineers at 0.65 focus, P80 ≈ 12 working days (~2.5 weeks)
plus 2 days of review and staged-rollout waits; earliest start Nov 3.
**Breakdown**
| Task | Low | Likely | High | Notes |
|---|---|---|---|---|
| Proration logic + tests | 2 | 3 | 6 | 20% risk: leap-year/downgrade rules unclear |
| Stripe price sync | 1 | 2 | 5 | 30% risk: annual proration in sandbox unverified |
| ... | | | | |
**Biggest uncertainty:** Stripe sync and proration (~90% of variance).
Half-day spike on Stripe proration on day 1 would narrow the range by ~3 days.
**Assumptions**
- Monthly plans unchanged; no grandfathering logic needed.
- Design for the plan picker is ready by Nov 3.
**Out of scope:** Invoicing changes for enterprise contracts.
**Re-forecast trigger:** after the spike, or if any task exceeds its high value.
Re-forecasting
- Track actuals per task. When a task exceeds its
high, re-estimate the remaining tasks that share its cause, not just that task. - Update the CSV with completed actuals (low = likely = high = actual) and rerun; the forecast tightens as work completes.
- Report changes early with the reason and the new range; do not absorb slips silently.
Gotchas
- Anchoring: the first number mentioned (often the requester’s hoped-for deadline) drags estimates toward it. Estimate tasks before looking at the target date.
- Forgetting the tail: review rounds, flaky CI, deploy, data backfills, and documentation account for a large share of real effort.
- Story points to days conversion without historical velocity is meaningless; use either calibrated points with measured velocity or ranges in days, not a mix.
- Optimism about integration: “just call their API” tasks have the widest real
distributions; give them wide ranges and
risk. - Estimates as commitments: label which number is the commitment (P80) and which is the target (P50). Say what scope could be cut to hold a date.
- Precision theatre: 13.4 days implies false precision. Round presented numbers.
Verification
- Every task is 0.5–3 days at
likely, or explicitly flagged as needing a split or spike. - The breakdown includes testing, review, rollout, and docs tasks.
- The presented forecast comes from the simulation, not summed
likelyor summedhighvalues, and states its confidence level and buffer. - Assumptions and out-of-scope items are listed; the top uncertainty has a plan to reduce it.
Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
scripts/estimate.py
#!/usr/bin/env python3
"""Monte Carlo roll-up of three-point task estimates.
Usage:
python3 estimate.py tasks.csv [--runs 20000] [--seed 7] [--risk-buffer 0.0]
tasks.csv columns (header required): task,low,likely,high[,risk]
low/likely/high : effort in any consistent unit (ideal days, hours)
risk : optional probability 0..1 that the task hits an extra
"surprise" equal to (high - likely) on top of its sample
Each task is sampled from a PERT (beta) distribution over [low, high] with
mode=likely. Prints per-task PERT mean, the total's P50/P80/P90/P95, and the
tasks contributing most variance. Standard library only. Exits 1 on bad input.
"""
from __future__ import annotations
import argparse
import csv
import random
import statistics
import sys
def pert_sample(rng: random.Random, low: float, mode: float, high: float) -> float:
if high == low:
return low
alpha = 1 + 4 * (mode - low) / (high - low)
beta = 1 + 4 * (high - mode) / (high - low)
return low + rng.betavariate(alpha, beta) * (high - low)
def load(path: str) -> list[dict]:
tasks = []
try:
with open(path, newline="", encoding="utf-8") as fh:
reader = csv.DictReader(fh)
required = {"task", "low", "likely", "high"}
if not reader.fieldnames or not required <= {f.strip() for f in reader.fieldnames}:
raise ValueError(f"header must include {sorted(required)}; got {reader.fieldnames}")
for i, row in enumerate(reader, start=2):
row = {k.strip(): (v or "").strip() for k, v in row.items() if k}
if not row.get("task"):
continue
lo, ml, hi = float(row["low"]), float(row["likely"]), float(row["high"])
if not (0 <= lo <= ml <= hi):
raise ValueError(f"line {i} ({row['task']}): need 0 <= low <= likely <= high")
risk = float(row["risk"]) if row.get("risk") else 0.0
if not 0 <= risk <= 1:
raise ValueError(f"line {i} ({row['task']}): risk must be 0..1")
tasks.append({"task": row["task"], "low": lo, "likely": ml, "high": hi, "risk": risk})
except (OSError, ValueError, KeyError) as exc:
print(f"error: {exc}", file=sys.stderr)
sys.exit(1)
if not tasks:
print("error: no tasks found", file=sys.stderr)
sys.exit(1)
return tasks
def percentile(sorted_vals: list[float], p: float) -> float:
k = (len(sorted_vals) - 1) * p
f = int(k)
c = min(f + 1, len(sorted_vals) - 1)
return sorted_vals[f] + (sorted_vals[c] - sorted_vals[f]) * (k - f)
def main(argv: list[str]) -> int:
ap = argparse.ArgumentParser(description="Monte Carlo roll-up of three-point estimates")
ap.add_argument("csv")
ap.add_argument("--runs", type=int, default=20000)
ap.add_argument("--seed", type=int, default=None)
ap.add_argument("--risk-buffer", type=float, default=0.0,
help="extra fraction added to every total for unlisted work, e.g. 0.15")
ns = ap.parse_args(argv)
if ns.runs < 1000:
print("error: use at least 1000 runs", file=sys.stderr)
return 1
tasks = load(ns.csv)
rng = random.Random(ns.seed)
totals: list[float] = []
per_task: list[list[float]] = [[] for _ in tasks]
for _ in range(ns.runs):
total = 0.0
for i, t in enumerate(tasks):
v = pert_sample(rng, t["low"], t["likely"], t["high"])
if t["risk"] and rng.random() < t["risk"]:
v += t["high"] - t["likely"]
per_task[i].append(v)
total += v
totals.append(total * (1 + ns.risk_buffer))
totals.sort()
naive_likely = sum(t["likely"] for t in tasks)
print(f"{'task':<40} {'low':>6} {'likely':>7} {'high':>6} {'risk':>5} {'PERT mean':>10}")
for t in tasks:
mean = (t["low"] + 4 * t["likely"] + t["high"]) / 6
print(f"{t['task'][:40]:<40} {t['low']:>6g} {t['likely']:>7g} {t['high']:>6g} {t['risk']:>5g} {mean:>10.1f}")
print(f"\nSum of 'likely' values: {naive_likely:.1f} (what single-point estimates would promise)")
if ns.risk_buffer:
print(f"Unlisted-work buffer applied: {ns.risk_buffer:.0%}")
print(f"Simulated totals over {ns.runs} runs:")
for label, p in (("P50", 0.50), ("P80", 0.80), ("P90", 0.90), ("P95", 0.95)):
print(f" {label}: {percentile(totals, p):.1f}")
p50 = percentile(totals, 0.5)
print(f" chance of finishing within sum of 'likely': "
f"{sum(1 for x in totals if x <= naive_likely) / len(totals):.0%}")
variances = sorted(
((statistics.pvariance(vals), tasks[i]["task"]) for i, vals in enumerate(per_task)),
reverse=True,
)
total_var = sum(v for v, _ in variances) or 1.0
print("\nLargest sources of uncertainty (share of variance):")
for v, name in variances[:5]:
print(f" {v / total_var:>4.0%} {name}")
print(f"\nSuggested commitment: P80 = {percentile(totals, 0.8):.1f} (median {p50:.1f})")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.