К основному содержанию
скилл CX Codex

task-estimator — скилл для Claude Code: оценка задач по вашей истории

Скилл считает сроки задач по вашей же измеренной истории, а не по наитию модели. Пригодится, когда надо назвать ETA для фичи, бага или миграции.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add marks97/task-estimator --skill task-estimator -a claude-code
Открыть на GitHub

Что делает task-estimator

Скилл ведёт журнал того, сколько задачи стоили на самом деле, и оценивает новые по эмпирическому распределению похожих прошлых задач. Дальше он сверяет свои же прошлые оценки с фактом, поэтому систематический заниженный прогноз становится виден. Работает в четырёх режимах: оценка, первичная настройка, запись фактических затрат по завершённой задаче и перекалибровка.

Журнал лежит вне каталога скилла, чтобы пережить переустановку: путь берётся из $CLAUDE_ESTIMATION_DIR, иначе ~/.claude/estimation. Внутри — ledger/chats.ndjson, ledger/tasks.ndjson, ledger/estimates.ndjson и calibration.json. Скрипты в $SKILL/scripts/ разбирают транскрипты, сшивают чаты в задачи, режут эпики на подзадачи и считают квантили. Запись и перекалибровка запускают record.mjs и calibrate.mjs вместе со score.mjs.

Если $LEDGER/calibration.json ещё нет, скилл скажет об этом и предложит настройку, но предварительную оценку по общим приорам всё равно даст. Молча считать по пустому журналу он не станет. Цикл закрывается только тогда, когда отработали все ноги: оценка пишет котировку, запись меряет факт, перекалибровка отвечает, были ли котировки хороши.

Какие задачи решает

  • назвать ETA для фичи или бага
  • проверить, реалистичен ли срок, который уже обещали клиенту
  • записать фактические затраты по завершённой задаче
  • перекалибровать оценки и увидеть систематическую ошибку
  • разложить эпик на подзадачи с часами по каждой

Примеры запросов агенту

  • сколько времени займёт добавление OAuth-провайдера?
  • запиши эту задачу в журнал
  • перезапусти калибровку и скажи, насколько точными были прошлые оценки

Как установить task-estimator

1Через Skills CLI (Vercel)

npx skills add marks97/task-estimator --skill task-estimator -a claude-code

2Вручную: положить файлы

~/.claude/skills/task-estimator/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/task-estimator/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Вопросы о task-estimator

Как установить task-estimator в Claude?

Выполните npx skills add marks97/task-estimator --skill task-estimator -a claude-code. Скилл окажется в ~/.claude/skills/task-estimator/SKILL.md.

Что делает task-estimator?

Считает срок задачи по вашей измеренной истории, а не по интуиции модели. Держит журнал фактических затрат и сверяет с ним собственные прошлые оценки.

Что нужно, чтобы оценки вообще работали?

Нужен непустой журнал: calibration.json с приорами и квантилями. Без него скилл даст только предварительную оценку по общим приорам и предложит настройку.

Исходный текст

SKILL.md · MIT
---
name: task-estimator
description: Estimate how long a coding task will take, using the user's own measured history instead of intuition. Use this skill whenever someone asks for an estimate, an ETA, a timeline or a deadline for a feature, bug, refactor, migration, script or investigation — "how long will this take", "give me an estimate", "how many hours is this", "can this ship today", "is this a one-day job", "how many days for the whole epic", "what should I tell the client", "estimate this ticket", "size this" — and equally when the ask is skeptical ("is that realistic", "why did that take three days"). Also use it for the machinery behind the estimates — setting up or calibrating estimation, backfilling the task ledger from past Claude Code transcripts, recording a finished task's actuals ("record this task", "log this one", "add this to the ledger"), and re-running calibration or checking whether past estimates were any good. Trigger it even when the word "estimate" never appears — a question about how much work something is, or how long something took, is this skill.
---

# Task estimator

Estimates from **your measured history**, not from the model's intuition about how hard
something looks. The model's intuition is the thing that is broken: developers using AI
tools believed they were 20% faster while measuring 19% slower (METR 2025). An estimate
that is not anchored to recorded actuals just reproduces that gap in writing.

So this skill does three things: it keeps a ledger of what tasks actually cost, it quotes
new tasks against the empirical distribution of similar past tasks, and it scores its own
past quotes so the bias is visible.

Depth on the evidence is in `references/`: `estimation-science.md` (the classical results
— EBS, reference classes, anchoring, flow metrics), `ai-era-evidence.md` (what changed once
an agent was in the loop), `measurement-model.md` (the phase taxonomy, the measured
constants, and the calibration math the scripts implement). Read them when a rule here looks
arbitrary — every rule traces to a result there. Do not re-derive the theory during an
estimate; execute the steps.

## Pick a mode

| The user is doing this | Mode |
|---|---|
| Asking how long something will take, ETA, sizing, "is this a day?" | **estimate** (default) |
| First run, no `$LEDGER` (see *Ledger layout*), "set this up", "calibrate me", "backfill" | **setup** |
| "record this task", "log this", finished a task, the CLAUDE.md hook fired | **record** — `record.mjs` |
| "re-run calibration", "how good are your estimates", "were you right", "recalibrate" | **recalibrate** — `calibrate.mjs` + `score.mjs` |

The four modes are one loop, and it only closes if every leg runs: **estimate** logs a
quote → **record** measures the actual and scores it against that quote → **recalibrate**
reports whether the quotes are any good → the next **estimate** corrects for it. Skipping
the log in estimate mode, or never running record mode, leaves the skill quoting from
history forever without ever learning whether its own quotes were right.

If `$LEDGER/calibration.json` does not exist and the user asks for an
estimate, say so and offer setup — then still give a prior-only estimate if they want one
now. Never silently estimate off an empty ledger.

## Ledger layout

`$LEDGER` resolves in this order, first hit wins:

1. `$CLAUDE_ESTIMATION_DIR` — explicit override.
2. `~/.claude/estimation` — the portable default.

The ledger lives outside the skill directory so it survives reinstalls, and
outside any per-account config dir so it is one history, not one per account.

```
$LEDGER/
  ledger/
    chats.ndjson       # one row per Claude Code conversation (mined)
    tasks.ndjson       # one row per unit of work; the reference class lives here
    estimates.ndjson   # one row per quote this skill emitted, for scoring
  calibration.json     # population priors + per-class empirical quantiles
```

Scripts (`$SKILL/scripts/`). The four primitives are pure and stdout-oriented:
`extract.mjs` (transcript JSONL → one chat row), `stitch.mjs` (chats → tasks),
`digest.mjs` (one chat → compact text for an LLM pass), `calibrate.mjs` (ledger →
calibration.json). `slice.mjs` sits between the last two: it cuts each epic's chats into
labelled sub-deliverables (`task.sub_tasks[]`) without ever splitting the epic row.

Two more close the loop, and they are the ones **record** and **recalibrate** run:
`record.mjs` (one finished session → extract → stitch → rework-merge → outcome →
score, appended to the ledger) and `score.mjs` (estimates × tasks → how good the
quotes were). Note the division of labour, because the two calibrations answer
different questions and get confused constantly:

| | Question | Reads | Writes |
|---|---|---|---|
| `calibrate.mjs` | how long do tasks *like this* take? | tasks.ndjson | calibration.json |
| `score.mjs` | were my *quotes* any good? | estimates.ndjson + tasks.ndjson | nothing |

`calibrate.mjs` is what an estimate quotes from. `score.mjs` is what tells you the
quotes are systematically low. A ledger with no `estimates.ndjson` rows can do the
first forever and never the second — which is the failure this loop exists to stop.

### estimates.ndjson — the row schema

One row per quote, written by **estimate** step 8, read by `record.mjs` and
`score.mjs`. `schema: "estimate.v1"`:

```json
{"schema":"estimate.v1",
 "estimate_id":"2026-08-31-add-oauth-provider",
 "date":"2026-08-31",
 "task":"add-oauth-provider",
 "ticket":null,
 "task_desc_hash":null,
 "features":{"type":"feature-epic","novelty":"adaptation","precedent":"saml-provider"},
 "class":{"neighbors":["example-api/TICKET-3880"],"n":1,"gate":"prior-only"},
 "quoted":{"agent_hours":{"p50":76,"lo":50,"hi":150},
           "human_rounds":{"p50":null,"lo":600,"hi":1000},
           "calendar_days":{"p50":null,"lo":28,"hi":49}},
 "source":"ledger-neighbor+bounded-adjustment"}
```

Field rules, each of which exists because breaking it makes the row unscorable:

- **`estimate_id`** is the join key `record.mjs --estimate-id` takes. Default it to
  `<date>-<task>`. Once a task is recorded against it, the id is written back onto
  the **task row** as `estimate_id`, so the link is never re-guessed.
- **`task`** is a slug, and it must be one `record.mjs` can find later: the ticket
  key, the branch name, or the phrase the classifier will summarise the work as.
  Matching runs `estimate_id → ticket → task_desc_hash → task_id slug → summary →
  epic-slice slug → sub-deliverable slug`, first hit wins. **A quote that matched an
  epic slice is scored against that slice's hours, not the epic's** — scoring "the
  webhook part" against a 76-hour integration would report a 60× miss on a quote that
  was never about the integration. `score.mjs` names the slice in `sub_task` when it
  does this.
- **`quoted`** carries the **three axes the ledger measures — `agent_hours`,
  `human_rounds`, `calendar_days` — and no others.** A band quoted in weeks is
  stored in days. Quote something on every axis you can: an axis with no band is
  silently unscorable, which is how an estimator ends up with a coverage number
  built from one axis.
- **`lo`/`hi` are the band you actually committed to** (the P50→P85 span, or the
  regime-split range). `score.mjs` scores coverage at 80% nominal against them.
- **`p50` may be null**, and then `score.mjs` uses the band's **log** midpoint
  (`sqrt(lo*hi)`) and flags every ratio derived from it — so a derived centre is
  never mistaken for something you said.
- **`task_desc_hash`** — sha256 of the anchor-stripped description. Optional but
  preferred when there is no ticket: it keeps prose out of the ledger while still
  matching later.

Legacy rows (the pre-schema hand-written shape: `agent_h_p50`, `agent_h_band`,
`rounds_band`, `calendar_weeks_band`) are normalised on read and still score. Write
new rows in the canonical shape.

**Not every real string is a real join signal.** `stitch.mjs` denies generic branches
(`main` is on every chat that ever pushed) and, for the same reason, unattested ticket keys:
a dated report filename pasted into a review round yielded `REPORT-2026` and welded 46 chats
and 79 agent-hours into one "task". A key only becomes a stitch edge if it is below the
load-bearing fan-out, or its prefix appears in a feature branch, or its namespace is *live* —
several sibling keys that also turn up **apart** from it, since co-pasted fragments always
travel together and real tickets do not. The rules and their evidence are in the comments on
`isPlausibleTicketKey` (extract.mjs) and `eligibleTicketKeys` (stitch.mjs).

Three drivers run a whole corpus and are **resumable** — re-running one continues, it never
restarts: `backfill.mjs` (walk a transcripts root → chats.ndjson + tasks.ndjson),
`classify.mjs` (haiku → classifications.ndjson, merged into tasks.ndjson),
`audit.mjs` (stratified sonnet re-judgement → audit.ndjson + per-field agreement).

## Mode: estimate

**The order below is mandatory.** Each step exists to stop a specific failure. Do not
reorder, do not skip ahead to "well, this looks like about a day".

### 1. Strip the anchors — before any reasoning

Read the task description. Before you think about duration at all, remove from
consideration: stated deadlines, target dates, story points, "should be quick / small /
huge / trivial", the user's own guess, a client's promised date, a ticket estimate field.

An irrelevant number in the prompt shifts estimates by **4–5×**, and the effect survives
being told to ignore it and survives coming from an obviously unqualified source. You
cannot un-see an anchor, so handle it explicitly: if the ask contained one, say
**"noting you said ~X — the number below is history-derived and was computed before
reading it"** and never let it move the quantile.

### 2. Readiness gate — refuse to estimate unready work

Do not produce an estimate if either is true:

- **No testable acceptance criteria.** Nobody can say what "done" is, in a way a test or a
  concrete observation could settle.
- **No converged implementation strategy.** Two or more materially different approaches are
  still live, and the choice changes the work by more than one bucket.

These are exactly the tasks that explode: the distribution is bimodal — ~78% of "easy"
tasks land near their class median, and the other ~22% overrun by more than 180%, and the
overrunners are overwhelmingly the ones that failed this gate.

Output instead:

> **Not estimable yet — recommend a timeboxed spike.**
> Missing: `<the specific criterion or the unresolved fork>`.
> Timebox: `<1 bucket on the ladder>`, and the deliverable of the spike is the missing
> thing, not the feature.
> Re-run the estimate after the spike; it will be a different (and much tighter) class.

Recommending a spike is a real answer. Producing a number for an unspecified task is not.

### 3. Extract features

Follow `prompts/estimate-features.md`. It is a checklist with the actual commands to run —
do the searches, do not guess the answers. It yields:

`repos[]`, `task_type` (bugfix|feature|refactor|migration|infra|investigation|script),
`greenfield|brownfield`, `precedent_in_repo` (did we ship this shape here before — find it
or state none), `self_verifiable` (tests|typecheck-only|human-eyes|prod-only),
`blast_radius` (grep-counted files likely touched), `external_deps[]` (third-party
approvals, other humans, deploys), `parallelizable` (across files/repos).

Precedent and self-verifiability do the most work of any features in the set. Spend the
searches on them.

Then place the task in the **speedup quadrant**, because the agent discount is not a
scalar:

| | Simple | Complex |
|---|---|---|
| **Greenfield** | +30–40% | +10–15% |
| **Brownfield** | +15–20% | 0–10%, sometimes negative |

Most work in a mature repo is bottom-right, where the discount is near zero and can invert.
Record the quadrant; step 6 uses it.

### 4. Retrieve the reference class

From `$LEDGER/ledger/tasks.ndjson`, in this order:

1. **k = 1–3 nearest neighbours** on the feature vector. Small k on purpose — larger k
   regresses to the mean and destroys what made analogy work.
2. If the tight class has **n < 5**, widen to the whole `task_type` class and say you
   widened.
3. If still thin, fall back to the population priors in `calibration.json` and say the
   estimate is prior-based.

**Check `field_reliability` before you lean on a field, and say what you find.** Every field
in it is either `reliable: true` or `reliable: false` with a measured agreement rate — there
is no unmeasured field. As of the 2026-09-01 audit `task_type` is the one that fails
(0.38 raw / 0.43 admissible, n=58 stride), so **widening to the `task_type` class is the
weakest move in this list and must be stated as such**: prefer the k=1–3 neighbours, and
when you do widen, say the class label itself agreed with a stronger judge only ~4 times in
10. It agrees 0.51 on well-stitched rows, so the fault is largely the 26% of rows that are
over-merged rather than the label.

Always record which class you landed in and its `n`. Report the **variance across the
neighbours**, not just the centre: 1h/1h/9h and 3h/3h/4h centre in the same place and mean
completely different things.

**When the ask is a slice of a bigger piece of work, use the `epic-slice` class instead.**
`calibration.json` carries `reference_classes["epic-slice"]` (with its own `by_task_type`),
built by `slice.mjs` from the sub-deliverables of every task with ≥6 chats. Reach for it
when the question is "how long is the webhook part of the payments integration" rather than
"how long is the payments integration" — a slice inherits an epic's already-loaded context, its
branch and its conventions, so it is **not** the same animal as a standalone task of the
same `task_type`. Say which of the two classes you quoted; they answer different questions
and the epic row remains the unit of record for both.

**With no usable class at all**, size the human-rounds axis from the constant-hazard model
rather than from feel: an agent's 80%-reliability horizon is ~30 minutes of
human-equivalent work, so `human_rounds ≈ ceil(human_equivalent_minutes / 30)`. Sanity band:
real sessions run **3.3–5.4 interventions**. A prior-only quote landing outside that band
needs a reason.

### 5. Compute empirical quantiles — never a fit, never a mean

Sort the class's actuals and take the quantiles **directly off the observed values**, in
log space. Do not fit a lognormal (or anything else) and read quantiles off the curve: the
fit systematically understates the tail, which is the only part of the distribution anyone
is actually asking about. Do not quote a mean; on a fat-tailed class the mean is neither
the typical case nor the risk.

**Check your own bias before quoting.** Run `node $SKILL/scripts/score.mjs` and read the
per-axis line: it is the only evidence about *this estimator* rather than about the tasks.

- If it prints **TOO EARLY TO CALL** (fewer than 5 scored pairs), quote the class straight
  and say the estimator itself is unvalidated. Do **not** apply a correction derived from
  one or two points — a single ratio is an observation, not a bias.
- Once it reports a geometric mean ratio over n ≥ 5, prefer **EBS ratio resampling**: draw
  whole historical estimate/actual ratios with replacement (100+ draws) and divide the
  class centre by each draw. Same principle — whole observed values, resampled, never
  averaged. Note this in `source` on the logged row so the provenance survives.

**Sample gates. State which one you are at, every time:**

| n in class | You may quote |
|---|---|
| < 5 | Nothing from the class. Prior-only, and say so explicitly. |
| 5–9 | **P50 only**, with a warning that there is no usable upper bound yet. |
| 10–19 | **P85**. |
| ≥ 20 | **P95** allowed. |

Below **n ≈ 20** the class quantile is also **shrunk toward the population prior** by
`w = n/(n+κ)` — a personal P85 at n=11 spans 0.31–2.26× of the truth, so it is worth
something as an adjustment and nothing on its own. `calibrate.mjs` emits the shrunk value;
use it, do not recompute the raw quantile and quote that instead.

15 fresh points beat 150 stale ones. `calibrate.mjs` already applies a 90-day half-life —
do not re-weight by hand.

### 6. Inside-view adjustment — at most one bucket

The log ladder: **≈15m · 1h · 3h · 1d · 3d · 1w+**.

You may move the class result by **at most one bucket**, and only while naming the
specific evidence: "the identical Prisma migration shipped twice this month, both under
2h". Unnamed intuition ("this feels simpler") buys **zero** adjustment. Two buckets is not
an adjustment, it is a different class — go back to step 4.

Then apply the **quadrant discount** from step 3 — and **widen the interval when you do,
never narrow it.** Speedup and variance move together: the quadrants with the biggest
expected gains also have the widest spread. "The agent will handle it" is a reason for a
wider band, not a tighter one.

Budget the **failure branch longer than the success branch**. Failed trajectories run
**18–82% longer** than successful ones; failure is expensive and late, not cheap and early.

Default to the class **P85**. Dropping to P50 requires naming the mitigation that removed
the tail: spec frozen, harness exists, no external dependency, precedent in this repo.

### 7. Output — the SLE, three axes, never a single number

```
**85% of your similar tasks finished in ≤ X agent-hours, ≤ Y human rounds, Z calendar days**
(class: <description>, n=<N>, neighbors: <task ids>)
```

Then, in this order:

- **P50 line** — "half finished in ≤ …", so the typical case is visible next to the limit.
- **Regime call** — the distribution is bimodal, not a spread around a centre. Say which
  mode you think this is in and why: *agent-nails-it* (precedent exists, tests verify,
  narrow blast radius) or *integration-thrash* (verification is human-eyes or prod-only,
  no precedent, external surface). Where both are live, give both with rough weights. A
  single blended number sits in the trough between the modes and describes almost nothing.
- **Blow-up risk flags**, each named, only the ones that apply: vague spec ·
  human-eyes-only verification · wide blast radius · external waits · no precedent in repo.
  Name the flag and the specific thing that triggered it.
- **External calendar waits, listed separately.** App review, a platform approval, someone
  else's deploy window, a human decision. This is **dead time, not work** — never fold it
  into agent-hours; it is why calendar days and agent-hours are separate axes.
- **The review tail is part of calendar days.** If done means merged, the estimate does not
  stop at "code written" — that is the part that got faster. Review time is up 441% and
  agent-authored PRs wait **5.3× longer** for pickup. Include the wait for a reviewer
  explicitly whenever the task ends in a PR someone else must merge.
- **"Split this"** whenever the estimate exceeds the class P85. Right-sizing beats
  budgeting more. Propose the actual split into ≤1-bucket pieces, do not just advise it.

Never emit a single number. A point estimate on a fat-tailed class is a category error,
and it is the format that makes people commit to dates they then miss.

### 8. Log the quote — not optional, and not later

**An estimate that was never logged can never be scored.** This is the step that makes
the skill improve instead of repeating itself, and it is the one that gets skipped
because the user already has their answer. Do it in the same turn you quote.

Append **one `estimate.v1` row** to `$LEDGER/ledger/estimates.ndjson` — schema and
field rules are in *Ledger layout*; the three axes and a `task` slug `record.mjs` can
find again are the load-bearing parts:

```bash
LEDGER=${CLAUDE_ESTIMATION_DIR:-~/.claude/estimation}
cat >> $LEDGER/ledger/estimates.ndjson <<'EOF'
{"schema":"estimate.v1","estimate_id":"2026-09-01-csv-export","date":"2026-09-01","task":"TICKET-1234","ticket":"TICKET-1234","features":{"task_type":"feature","greenfield":false,"precedent_in_repo":"example-api/TICKET-1180","self_verifiable":"tests","blast_radius":9},"class":{"neighbors":["example-api/TICKET-1180","example-api/TICKET-1204"],"n":7,"gate":"p50-only"},"quoted":{"agent_hours":{"p50":3,"lo":1.5,"hi":9},"human_rounds":{"p50":12,"lo":6,"hi":30},"calendar_days":{"p50":1,"lo":1,"hi":4}},"source":"class-p50+one-bucket-precedent-adjustment"}
EOF
```

Then close the estimate by telling the user the loop is armed, in one line: *"logged as
`2026-09-01-csv-export` — run record mode when it lands and it scores itself."*

Two rules about the numbers in that row:

- **Log what you told the user, not a hedged version.** A band quietly widened in the
  ledger manufactures coverage the estimator never earned.
- **Never edit a logged row once work has started.** A quote revised with knowledge of
  the outcome is not a quote. Log a second row and let both score.

Store the **hash**, not the text — the ledger stays free of task prose, and `record` mode
matches on it later to score ratio (actual/quoted) and interval coverage.

### Doctrine — state this when the shape of the estimate is questioned

> **duration ≈ rounds × (per-round agent time + per-round human wait)**
>
> **Human round count is the dominant predictor (r ≈ 0.83 on measured data). First-turn
> duration predicts nothing (r ≈ 0.0).**

So a task that one-shots in 40 minutes and a task that takes eight rounds of "not quite,
try again" are not comparable, however similar the diff looks. Estimate the rounds; the
minutes follow. And the per-round human wait is the user's own latency — it is in the
recorded actuals already, do not model it separately.

Two corollaries worth saying out loud when they apply:

- **A long first turn is a good sign, not a bad one.** It predicts *fewer* rounds —
  front-loaded framing pays for itself. Never quote a task as slow because it started slow.
- **Waiting for permission prompts is the largest recoverable loss.** Approval gaps are 52%
  of long intra-task pauses, median 32 minutes each. If the user's setup asks for approval
  per tool call, that — not model speed — is what is costing them the afternoon.

Estimate **rounds and steps, never lines**. Assistant steps correlate with duration at
r ≈ 0.86 and human rounds at r ≈ 0.82; LOC manages only 0.53. A diff-size estimate is a
worse predictor than the thing it feels more concrete than.

**Never accept self-reported speedups as calibration input.** "That took me 20 minutes" is
not data; it is the perception gap talking. Only mined actuals go in the ledger.

## Mode: setup

### 1. Structure

Check `$LEDGER` (resolution order in *Ledger layout*). Create `ledger/`, empty `chats.ndjson` / `tasks.ndjson` /
`estimates.ndjson`, and a placeholder `calibration.json`. Do not overwrite anything that
exists — if the ledger already has rows, you are in **recalibrate**, not setup.

### 2. Consent — before touching any transcript

**Stop and ask with AskUserQuestion.** Do not read a single transcript before an answer.
Explain exactly this:

- It reads Claude Code conversation JSONL under `~/.claude*/projects/` (both accounts) from
  this machine only.
- Nothing leaves the machine except the classification calls, which go to the Anthropic API
  as `claude -p` — the same place the user's normal sessions already go.
- What gets stored: task shape and durations. Not the conversation text.
- It is reversible: `rm -rf ~/.claude/estimation`.

Offer to scope it: all projects, one project, or the last N months.

### 3. Backfill — deterministic pass

One command over the whole transcripts root — it walks every project dir, extracts each
`.jsonl` **sequentially** (never parallel: these files run to 140 MB and this is a live
box), and stitches globally at the end:

```bash
nice -n 19 ionice -c3 node --max-old-space-size=2048 $SKILL/scripts/backfill.mjs \
  --root ~/.claude/projects
```

It skips transcripts under 5 KB as noise (consent probes, sessions that never got past the
banner) and counts them; it skips any chat whose file **and sidecar directory** are both
unchanged since the last run. Interrupt it and re-run — chats are appended as they are
extracted.

Scratch and its progress log go in `/tmp/claude/task-estimator-backfill/`, never in a repo
and never in the ledger.

**Subagent sidecars are mined too, and they are where the delegated work is.** Next to
`<chat_id>.jsonl` the harness writes `<chat_id>/subagents/agent-*.jsonl` plus a
`.meta.json` naming each agent's type. `extract.mjs` runs the **same** extraction core over
each one and folds the result into the parent chat's `subagents` object —
`loc_added`/`loc_removed`, `files_edited`, `repos`, `active_min`, `sidecar_count` and a
`per_type` breakdown. This is not an optional refinement: a `Task`/`Agent` tool_result in
this Claude Code era carries no `toolStats`, so a chat that delegated everything reads as
zero LOC without it, and the parent does not even persist every spawn (one 104-spawn sprint
kept two). Two rules hold absolutely:

- **Nothing folds into the parent's own fields.** A subagent's minutes run *inside* its
  parent's wall clock, so adding them would count time that was spent once, twice. The
  chat's `loc_added`, `files_edited` and `agent_active_min` never move.
- **Nothing is counted twice inside `subagents` either.** Where the parent *did* report
  `toolStats` for a spawn and a sidecar for that same `toolUseId` exists, the sidecar
  measurement **replaces** the reported one — it is the primary record. Spawns with no
  sidecar keep their reported numbers. Depth-2 agents (spawned by another subagent, so no
  `tool_use` block exists in the parent at all) are added, which is why `subagents.count`
  can exceed the number of spawn blocks.

Sidecars are never emitted as chats of their own.

**Every chat also gets a `repo_share`** — `{repo: fraction}`, summing to exactly 1,
splitting its minutes across repos in proportion to the files edited there (parent edits
plus subagent edits). Chats that edited nothing attribute wholly to their cwd repo, or to
the workspace when the cwd is a multi-repo root. `stitch.mjs` rolls this into
`agent_hours_by_repo` / `subagent_hours_by_repo` on the task row. **Use those for any
per-repo or per-area question.** Summing `agent_hours` over `repos` instead counts every
multi-repo chat once per repo — on this corpus that inflated an area roll-up ~25x.

**Probe sessions get a `noise` flag** — `"permtest"` (a permission probe: `echo
PERMTEST_OK`, "testing perms here") or `"qa-probe"` (a session run out of a `/tmp` scratch
tree). Detection is ratio-based, not keyword-based, because real chats discuss permission
probes at length; a chat is a probe only when probing is essentially all it did. The rows
stay in the ledger and `calibrate.mjs` drops them from the quantiles.

### 4. Backfill — semantic pass

One classification per chat: `digest.mjs` → haiku. **Before running, count the chats,
estimate the token spend, print it, and ask to proceed.** A backfill over a year of
transcripts is not free and the user gets to see the number first — `--estimate-only`
prints exactly that and does nothing else.

```bash
node $SKILL/scripts/classify.mjs --estimate-only      # count + token/cost/wall estimate
node $SKILL/scripts/classify.mjs --concurrency 3      # then, once they agree
```

`classify.mjs` drives this call per chat, with `MAX_THINKING_TOKENS=0` (measured: 6.5 s and
$0.007 per call against 44 s and $0.025 with thinking, no loss against the prompt's rules —
the audit stage is what keeps that honest):

```bash
node $SKILL/scripts/digest.mjs --chat <chat-id> \
  | claude -p --model claude-haiku-4-5 \
      --output-format json \
      --system-prompt "$(cat $SKILL/prompts/classify-chat.md)" \
      --tools "" --safe-mode --no-session-persistence \
  | jq -r '.result'
```

Every flag is load-bearing:

- `--tools ""` — no tool loop. It is a classifier, it reads text and returns JSON.
- `--safe-mode` — no CLAUDE.md, skills, hooks, MCP servers. Keeps the classifier
  uncontaminated by project instructions and keeps the prompt prefix stable, so the batch
  gets prompt-cache hits. Auth still works normally (unlike `--bare`).
- `--no-session-persistence` — **critical.** Without it every classification writes a new
  JSONL into `~/.claude*/projects/`, which the next backfill would mine as a chat. The
  ledger would start eating its own tail.
- `--output-format json` returns an envelope; the model's text is `.result`, and it may
  still arrive fenced. Strip ``` fences before parsing. Optionally harden with
  `--json-schema` using the Schema block from the prompt file.

Merge the returned fields into the task row, plus `classified_at` and `classifier_model`.
A task has several chats, so merging has rules:

- **Labels come from the main chat** — the classification that called itself `main`,
  tie-broken by agent-active minutes. Labels do not vote: averaging a bugfix and a refactor
  produces neither.
- **`outcome` is decided by the artifact, not by the classifier.** A task with `commits > 0`
  or a non-empty `prs` is `shipped`, full stop — those fields come from git and `gh` output,
  not from a model reading a truncated digest, and they are exactly the standard *done means
  merged* sets. The classifier keeps only the two verdicts an artifact cannot express:
  `shipped-then-reverted` and `abandoned` (in both, the commit exists *and* something else
  happened). `outcome_source` records `deterministic` or `classifier` and
  `classifier_outcome` keeps the original, so an audit can still score the model.
  This exists because the classifier ran **2.3:1 too pessimistic** — a sonnet audit moved 62
  rows `unknown → shipped` against 27 the other way, all of it the "prefer unknown over a
  guess" rule firing on a digest that simply did not show the commit line.
- **`extern...

© marks97 · MIT · marks97/task-estimator

Похожие скиллы

Все скиллы по теме «Настройка агента: память, контекст, субагенты» →
groove-utilities-task-analyse andreadellacorte Собирает задачи из бэкенда проекта и раскладывает их по статусам, чтобы вечером было чем заполнить дневную память. CX Codex +1 588установок groove-utilities-task-list andreadellacorte Показывает активные задачи из настроенного бэкенда: что в работе, что готово к старту, что заблокировано. Удобно в начале дня. CX Codex +1 577установок task-breakdown jwynia Помогает разложить парализующую задачу на мелкие шаги, когда не получается начать. Опирается на стратегии для ADHD и аутизма. CX Codex +1 476установок project-estimation aj-geddes Скилл для оценки объёма проекта, сроков и ресурсов. Пригодится, когда нужно посчитать бюджет или защитить план перед заказчиком. CX Codex +1 659установок thinking-probabilistic tjboudreaux Скилл заставляет агента называть базовую ставку, давать интервалы и пересчитывать вероятность при новых данных вместо уверенных одиночных чисел. CX Codex +1 591установок ultragoal Yeachan-Heo Делит бриф на упорядоченные цели и ведёт дописываемый журнал в .omc/ultragoal, чтобы прогресс пережил перезапуск сессии. CX Codex +1 502установок

Спонсоры проекта

Компании, которые помогают держать аналитику открытой