Effective ETA
A trustworthy ETA is not a guess about the future. It is arithmetic over counted work units, corrected by actuals you recorded earlier. This skill is the whole procedure: read it and start applying it, no setup, no scripts, no dependencies.
Headline unit is wall-clock time to delivery - when the human has the finished thing, including builds, CI, and waiting. Steps remaining and cost are secondary fields, never the headline.
The three rules
- Never publish a bare number. Always a p50 headline plus a p80 upper bound. A single number is a promise you cannot keep.
- Never re-baseline silently. If the number grows, say the old one, the new one, and the cause. A quietly updated ETA is worse than no ETA.
- Never let ETA bookkeeping block, delay, or fail the actual work. If any part of this procedure is impossible in your environment, degrade to the simplest tier that works and say so in one clause. Do not stop to ask.
When to run this
Run it when: the task has 3+ steps or looks longer than ~10 minutes; the user asks how long, when, or how much; you report to a parent agent; or the work has slipped past a number you already gave.
Skip it when: the task is a single obvious edit, an answer, or a lookup. Announcing "ETA ~2m" on a one-line change is noise. Just do the work.
The loop
Decompose, estimate, publish, track, re-forecast. Steps 1 to 3 happen once at plan time. Steps 4 and 5 repeat until done.
1. Decompose
Break the work into steps that are each 5 to 60 minutes and have a verifiable done-condition ("tests pass", "page renders", "PR merged"). A step you cannot tell the end of is a step you cannot estimate. If a step would exceed 60 minutes, split it. If it is under 5, merge it into a neighbour.
Agents systematically omit the same steps. Add every one of these that applies, because they are where estimates die:
- Verification - running it, looking at it, driving the real surface.
- The fix round - the first attempt failing and needing a second pass. Budget one per non-trivial step; that is the honest base rate, not pessimism.
- Integration - wiring a finished piece into the thing that calls it.
- Waiting - CI, builds, deploys, propagation, human review.
- Setup - credentials, access, environments you do not have yet.
Mark each step's dependency (after: 3) when order is forced, and its
owner if subagents will run steps concurrently.
Tag steps that block on someone other than you as WAIT. They count toward wall-clock but not toward your working time, and they behave completely differently under calibration, so keep them separate.
2. Estimate each step
For every step, in this order:
a. Pick a reference class. Full table in
references/reference-classes.md. The common ones, as low / likely / high in
minutes:
| Class | low | likely | high |
|---|---|---|---|
| single-file change | 3 | 8 | 20 |
| multi-file change (2-5 files) | 8 | 20 | 50 |
| refactor across a module | 15 | 40 | 120 |
| new feature, vertical slice | 25 | 60 | 180 |
| bug fix, cause known | 10 | 25 | 70 |
| bug fix, cause unknown | 15 | 45 | 180 |
| run test suite | 1 | 5 | 20 |
| codebase research sweep | 5 | 15 | 45 |
| deploy and verify live | 5 | 15 | 60 |
| WAIT: CI round trip | 5 | 12 | 40 |
| WAIT: human review | 15 | 60 | 480 |
| unknown, not yet scoped | 5 | 20 | 90 |
b. Apply friction multipliers to likely and high (not to low):
| Condition | x |
|---|---|
| Codebase unfamiliar to you | 1.5 |
| No tests covering the area | 1.3 |
| No types, or legacy style | 1.4 |
| First time using this API or tool | 1.5 |
| Access or credentials not yet in hand | 2.0 and flag as a risk |
Multipliers compound, but cap the product at 3.0. Beyond that you are not estimating, you are guessing, and the honest move is to make step 1 a timeboxed spike and re-estimate the rest after it.
c. Adjust the range, never the point. If you are unsure, widen high.
Never drop a step because you cannot size it - an unsized step becomes
unknown at 5 / 20 / 90 and gets flagged.
d. Apply your calibration factor (from step 5, below). On a first run there is none, so the factor is 1.0 and the prior table carries the estimate. That is the point of the table: the very first ETA is still defensible.
3. Compute the ETA
All arithmetic here is doable by hand. Work in minutes.
Per step, expected value (PERT mean):
mean = (low + 4 x likely + high) / 6
Per step, spread:
spread = (high - low) / 6
Serial total (one agent, steps back to back):
total_mean = sum of all remaining means
Combined spread - spreads do not add, they root-sum-square. Take the sum of spreads and divide by the square root of the number of steps:
independent = (sum of spreads) / sqrt(N)
| N | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 |
|---|---|---|---|---|---|---|---|---|
| sqrt(N) | 1.41 | 1.73 | 2.0 | 2.24 | 2.45 | 2.83 | 3.16 | 3.46 |
That formula assumes step errors are independent, and they are not. If the codebase is worse than you thought, or you misread the architecture, or the model is having a bad day, every step runs long together. Left uncorrected, root-sum-square makes a six-step plan look far more certain than any six-step plan has ever been. So apply a correlation floor:
total_spread = whichever is larger of:
independent
0.25 x total_mean
The two published numbers:
p50 = total_mean
p80 = total_mean + 0.84 x total_spread
The floor means p80 is never less than about 1.2x p50. If your own arithmetic ever produces a tighter band than that, the arithmetic is wrong, not the work.
Parallel work. If subagents run steps concurrently, the serial sum is wrong. Use:
eta = max(critical_path, total_mean / agent_count)
critical_path is the longest chain of dependent steps, summing their means.
You can never beat it no matter how many agents you add. Compute both, take
the larger. WAIT steps sit on the critical path in real time even though they
cost you no working time.
Rounding - this is where false precision creeps in. Round the published numbers:
| Magnitude | Round to |
|---|---|
| under 10 min | 1 min |
| 10 to 60 min | 5 min |
| 1 to 8 hours | 15 min |
| over 8 hours | 1 hour, or working days at 6 productive hours/day |
"ETA 47.3 minutes" is a tell that the number is fabricated. "ETA ~45m" is honest about its own resolution.
Cost, as a secondary field. Estimate tokens per step from its class (research and long-context steps are input-heavy; code generation is output-heavy), multiply by your model's per-million pricing, and give one rounded figure for the whole job. Label it an estimate. If you do not know the model's pricing, report token counts instead and say pricing is unknown. Never invent a price.
4. Publish
Progress bar - required on every human-facing publish, and it goes first. A bar is read in one glance where a line of numbers is skimmed and forgotten, and it is the one place the p80 band becomes something a person can see rather than parse.
[########------------]........ 40% | ~45m left (p80 1h 15m) | 2/6 steps
Read it as: the 20 cells inside the brackets are the expected (p50) job,
5% each. # is finished, - is what remains, and the closing bracket is the
p50 finish line. The . cells past it are the p80 tail - the overrun the
band allows for. The work is expected to end at the bracket and could run into
the dots.
The arithmetic, all hand-doable, using the same numbers you already have:
done = sum of ACTUALS for completed steps (+ measured elapsed on the active step)
span = done + p50 <- p50 is remaining work, so this is the whole job
pct = 100 x done / span
filled = round(pct / 5) capped at 20
tail = round(20 x (p80 - p50) / span) capped at 10
Rules that keep the bar from lying:
- Weight by effort, never by step count. A six-step job whose step 3 is half the work is not 33% done when step 2 lands. Count minutes, not steps.
- No partial credit you did not measure. An in-flight step contributes only the elapsed time you can actually read off a clock or a timestamp. Guessing "about half done" is how bars get stuck at 90%.
- The bar may go backwards, and when it does that is a slip, not a rendering choice. Re-forecasting more remaining work lowers the percentage; publish the slip report from step 5 alongside it. Never quietly hold the percentage still to avoid the conversation.
- Never show 100% before it is done. Cap the display at 99% until the done-condition of the last step is actually verified.
- Under parallelism, measure the critical path, not the pile of total work.
The bar shows what the human is waiting through, so
doneandspanboth come from the critical path. Three subagents finishing at once advance it by the longest of the three, not by the sum.
The correlation floor puts a floor under the tail too. At kickoff nothing is
done, so span equals p50 and the tail is at least 20 x 0.21 = 4 cells.
A bar that starts with fewer than four dots is arithmetic gone wrong, not a
job that is unusually certain. The tail then shrinks as work lands, because
less of the job is left to be wrong about. If it would ever exceed 10 cells,
render 10 and end the run with > to mark the overflow.
Variants for the states that are not plain progress:
[############--------].... 60% | BLOCKED on review | clock stopped 12m ago
[####################] 100% | done in 1h 5m (est ~45m, p80 1h 15m)
ASCII is the canonical form because it survives every pipe, log, and parent
agent. Where the surface reliably renders Unicode you may upgrade the glyphs to
█ filled, ░ remaining, · tail, keeping the same widths and arithmetic.
Human line - one line under the bar, no ceremony, same moment as the bar:
[########------------]........ 40% | ~45m left (p80 1h 15m) | 2/6 steps
ETA ~45m (p80 1h 15m) | 2/6 steps | next: update the callers | est ~$0.60
Include calib x1.2 (n=5) only when a calibration factor is actually applied.
Notification sync - on every publish (plan, update, slip, completion), write the one-line ETA to the notification state file so any push (e.g. a Telegram ping for a Hermes session or a coding-agent watchdog) carries the live estimate instead of a static "task completed". The line is the human line, compacted to one line, HTML-escaped is NOT needed (the ping escapes it):
~/.hermes/eta_state <- write: 45m (p80 1h 15m) | 2/6 steps | next: update the callers
If you have a shell, write it with one command and do not block on it:
printf '%s' "<line>" > ~/.hermes/eta_state. If you have no filesystem,
skip it - the in-chat bar remains the source of truth. On completion, clear the
file (an empty/missing file means no in-flight ETA): rm -f ~/.hermes/eta_state.
Do not let this step block, delay, or fail the work (rule 3).
Parent-agent block - same numbers, parseable, still plain text:
status: in_progress
p50_minutes: 45
p80_minutes: 75
steps_done: 2
steps_total: 6
percent_complete: 40
next: update the callers
blocked_on: none
cost_estimate_usd: 0.60
confidence: medium
basis: reference-class + calibration n=5
updated: 2026-08-05T14:02Z
confidence is low when any step is class unknown, when a friction
multiplier hit the 3.0 cap, or when you have fewer than 3 calibration samples.
percent_complete is the same effort-weighted figure the bar is drawn from.
Send the number, not the bar: a parent agent parses fields and renders its own.
The ledger - your durable memory of the plan. Keep it as a markdown table.
Hold it in context for short jobs; write it to a file for long ones, so it
survives context compaction and handoff between agents. Append to the project's
existing tracker if there is one; otherwise ETA.md at the working root.
| # | Step | Class | l/m/h | After | Status | Actual | Note |
|---|---|---|---|---|---|---|---|
| 1 | Map auth call sites | research | 5/15/45 | - | done | 22 | more sites than expected |
| 2 | Add token refresh | single-file | 3/8/20 | 1 | done | 8 | |
| 3 | Update callers | multi-file | 8/20/50 | 2 | active | - | |
| 4 | Run test suite | test-run | 1/5/20 | 3 | todo | - | |
| 5 | Fix round | fix-round | 5/15/40 | 4 | todo | - | |
| 6 | Deploy and verify | deploy | 5/15/60 | 5 | todo | - | |
Statuses: todo, active, wait, blocked, done, dropped. Dropped steps
stay in the table with their reason. Deleting them hides the scope change.
The Actual column is what the bar is drawn from, and this ledger is the state
published above: 22 + 8 = 30 minutes done against 45 remaining, so
30 / 75 = 40%, eight filled cells. Every published figure in this section
traces back to this table, which is the reason the table is worth keeping.
5. Track and re-forecast
Knowing the time. You cannot feel time passing. Get it from a real source, in this order:
- Timestamps already present in tool output, logs, or file mtimes.
- A clock command if you have a shell (
date -u +%FT%TZ). - Neither: switch to work units. Report steps and tool-call counts as the
headline ("4 of 9 steps, roughly halfway"), give the minutes figure as
derived, and mark it
clock: unverified. Never present a wall-clock number you had no way to measure as though you measured it.
Record the actual when a step finishes. One number, in minutes, in the ledger. This is the entire input to calibration, and skipping it is what makes every later estimate worthless.
Calibration. After 3+ completed steps, take the ratio actual / mean for
each and use their median (median, not average - one pathological step
should not move it). Shrink it toward 1.0 so a small sample cannot swing wildly:
factor = 1 + (median_ratio - 1) x n / (n + 3)
With n = 3 and a median ratio of 2.0, factor = 1.5. With n = 12 and the same ratio, factor = 1.8. Clamp to the range 0.5 to 3.0. Multiply every remaining step's mean by it before recomputing p50 and p80.
Keep WAIT steps out of the calculation. Waiting on CI does not tell you anything about how fast you write code.
Persist finished-task ratios per repo, appended to .eta/history.md as
date | class | estimate | actual. Next task in the same repo starts already
calibrated instead of starting over. Format is in
references/reference-classes.md.
When to re-publish. Not every turn - that is noise. Every publish re-renders the bar from the current numbers; a stale bar left standing next to a fresh ETA is the same silent re-baseline that rule 2 forbids. Publish when:
- the plan is first made;
- a step completes or changes status;
- the p50 moves by more than 25% in either direction;
- you become blocked, or unblocked;
- always when the new p50 passes a number you already promised.
Slip reports. A slip is reported, never absorbed. State the old number, the new number, and which cause:
scope- work was added.discovery- the work was bigger than it looked.estimate- the sizing was simply wrong.blocked- waiting on something outside your control.
[####----------------]....... 20% | ~1h 10m left (p80 1h 40m) | 1/6 steps
ETA slip: was ~45m, now ~1h 10m (p80 1h 40m).
Cause: discovery - auth is called from 9 files, not the 3 the grep suggested.
Steps 3 and 5 re-sized; the rest unchanged.
That one finished step took 18m, so the bar read 29% before the discovery (18 of an expected 63) and reads 20% after (18 of 88). The same work is done; the job grew underneath it. Showing the drop is the point. A bar that only ever advances is decoration.
Finishing early gets reported the same way. An ETA that only ever moves out is a padded ETA, and people learn to discount it.
On completion, report actual versus the first published p50. That closing comparison is what makes the next ETA credible, and it costs one line:
[####################] 100% | done in 1h 5m
Done in 1h 5m (first estimate ~45m, p80 1h 15m - inside the p80 band).
The final bar is the only one that shows 100%, it has no tail, and it is drawn only once the last done-condition has actually been verified.
Degradation
Every one of these is a "keep going and note it", never a stop.
| Situation | Do this |
|---|---|
| No clock of any kind | Headline in steps and work units; mark clock: unverified. The bar survives, because it is drawn from estimated effort rather than measured time - fill it from the means of completed steps and drop the "time left" clause. |
| Steps have no effort estimates at all | Fall back to a step-count bar and label it (by steps) in the same line, so nobody reads it as effort. This is the weakest form of the bar and the label is what keeps it honest. |
| Output is not fixed-width (proportional font, narrow or wrapping surface) | Drop the bar rather than ship a misaligned one, and publish 40% · ~45m left (p80 1h 15m) on its own. A bar that wraps mid-run reads as corruption. |
| No filesystem writes | Keep the ledger inline in your output. Re-post it in full when you re-forecast, so it survives compaction. |
| No calibration history | Factor 1.0, prior table, confidence: low. Say it is a first estimate. |
| Task cannot be decomposed yet | Publish an ETA for a timeboxed spike only, plus the explicit note that the rest is unscoped. Never extrapolate a total from nothing. |
| Ledger lost to compaction or a crash | Rebuild from completed work visible in the transcript, mark it reconstructed, keep going. |
A step blows past its high |
Stop estimating that step. Re-classify it, re-decompose it, and report a slip with cause discovery. |
| User wants one number, no range | Give p80 as the single number, not p50. Under-promise is the only safe direction. |
| Work is open-ended by nature | Publish a rate ("~6 files/hour, 40 remaining") instead of a completion time, and say why a completion time would be fiction. |
Non-goals
This skill estimates, records, and reports. It does not schedule work, choose what to work on, spawn or manage agents, enforce deadlines, or pad numbers to look good. It never blocks the underlying task.
Further reading
references/reference-classes.md- the full prior table, friction multipliers, class definitions, and the per-repo history format.references/worked-examples.md- three end-to-end runs: a plain serial task, a mid-task slip and re-forecast, and a parallel multi-agent job.