关于 task-estimator
task-estimator 是面向开发者的 Claude Code 技能,依据记录的实际工作历史估算任务,而非模型直觉。它维护任务账本、参考相似任务给出估算,并评估估算表现以进行校准。
主要功能
- 01记录对话、任务和估算账本
- 02参考相似历史任务估算
- 03记录已完成工作及实际结果
- 04评估估算准确度
- 05根据账本重新校准估算
适用场景
- 01依据项目历史估算任务
- 02完成任务后记录实际耗时
- 03检查过往估算是否准确
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ marks97/ task-estimator --skill task-estimator安装后在 Claude Code 中输入/task-estimator确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdprompts/audit-task.mdclassify-chat.mdestimate-features.mdreferences/ai-era-evidence.mdestimation-science.mdmeasurement-model.mdscripts/audit.mjsbackfill.mjscalibrate.mjsclassify.mjsdigest.mjsextract.mjslib.mjsrecord.mjsscore.mjsslice.mjsstitch.mjstemplates/claude-md-snippet.md详细介绍
任务估算器
依据你实际测量的历史数据进行估算,而不是依据模型对任务难度的直觉。模型的直觉正是问题所在:使用 AI 工具的开发者认为自己快了 20%,而实际测量结果却显示慢了 19%(METR 2025)。没有以记录的实际数据为依据的估算,只会把这种差距照搬到文字中。
因此,此技能会做三件事:记录任务的实际耗时;根据类似历史任务的经验分布,为新任务报价;并对过去的报价进行评分,让偏差显现出来。
证据的详细说明见 references/:estimation-science.md(经典研究结果——EBS、参考类别、锚定、流量指标)、ai-era-evidence.md(Agent 参与后发生了哪些变化)、measurement-model.md(阶段分类、测量常数,以及脚本实现的校准计算)。如果这里的某条规则看起来武断,请阅读这些文档——每条规则都有相应研究结果作为依据。估算时不要重新推导理论;按步骤执行即可。
选择模式
| 用户正在做什么 | 模式 |
|---|---|
| 询问某件事需要多长时间、ETA、规模估算、“这要一天吗?” | estimate(默认) |
首次运行、没有 $LEDGER(见账本布局)、“设置一下”、“校准我”、“回填” | setup |
| “记录这个任务”、“记一下”、任务已完成、触发了 CLAUDE.md 钩子 | record — record.mjs |
| “重新运行校准”、“你的估算有多准”、“你说对了吗”、“重新校准” | recalibrate — calibrate.mjs + score.mjs |
四种模式构成一个闭环,只有每个环节都运行才算闭合:estimate 记录报价 → record 测量实际耗时并根据报价评分 → recalibrate 报告报价是否准确 → 下一次 estimate 据此进行修正。在 estimate 模式下跳过记录,或从不运行 record 模式,都会导致此技能永远依据历史数据报价,却始终不知道自己的报价是否准确。
如果 $LEDGER/calibration.json 不存在,而用户要求估算,请说明这一点并提供设置选项——如果用户现在就想要估算,仍要给出仅基于先验数据的估算。切勿在空账本上悄无声息地进行估算。
账本布局
$LEDGER 按以下顺序解析,采用第一个匹配项:
$CLAUDE_ESTIMATION_DIR— 显式覆盖项。~/.claude/estimation— 通用默认路径。
账本位于技能目录之外,因此重新安装后仍会保留;也位于任何按账户划分的配置目录之外,因此所有账户共用一份历史记录。
$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
脚本($SKILL/scripts/)。四个基础工具均为纯函数,并面向 stdout 输出:
extract.mjs(转录 JSONL → 一条聊天记录)、stitch.mjs(聊天记录 → 任务)、
digest.mjs(一条聊天记录 → 供 LLM 处理的精简文本)、calibrate.mjs(账本 →
calibration.json)。slice.mjs 位于最后两者之间:它会将每个 epic 的聊天记录切分为带标签的子交付项(task.sub_tasks[]),但绝不会拆分 epic 记录本身。
另外两个工具负责闭环,也是 record 和 recalibrate 运行的工具:
record.mjs(一个已完成的会话 → extract → stitch → rework-merge → outcome →
score,并追加到账本)和 score.mjs(估算 × 任务 → 报价准确度)。请注意两种校准各自的职责,因为它们回答的是不同问题,却经常被混为一谈:
| 问题 | 读取 | 写入 | |
|---|---|---|---|
calibrate.mjs | 类似这样的任务需要多长时间? | tasks.ndjson | calibration.json |
score.mjs | 我的报价准确吗? | estimates.ndjson + tasks.ndjson | 无 |
估算时引用的是 calibrate.mjs 的结果。score.mjs 则会告诉你报价是否存在系统性偏低的问题。如果账本中没有 estimates.ndjson 记录,前者可以一直运行,后者却永远不会运行——而这个闭环正是为了防止这种情况。
estimates.ndjson — 行架构
每条报价占一行,由第 8 步 estimate 写入,供 record.mjs 和 score.mjs 读取。schema: "estimate.v1":
{"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"}
字段规则;每条规则存在的原因都是违反它会导致该行无法评分:
estimate_id是record.mjs --estimate-id使用的关联键。默认值设为<date>-<task>。将任务记录到该 ID 后,ID 会写回 任务行,作为estimate_id,因此不必再猜测关联关系。task是一个 slug,且必须是record.mjs之后能够找到的内容:工单 键、分支名称,或分类器会用来概括这项工作的短语。匹配顺序为estimate_id → ticket → task_desc_hash → task_id slug → summary → epic-slice slug → sub-deliverable slug,取第一个匹配项。如果报价匹配到史诗切片,就按该切片的工时评分,而不是按整个史诗评分——把「webhook 部分」按 76 小时的集成工作评分,会让一份原本与整个集成无关的报价出现 60 倍的误差。执行此操作时,score.mjs会在sub_task中标明该切片。quoted包含账本衡量的**三个维度——agent_hours、human_rounds、calendar_days,除此之外不含其他维度。**以周为单位的区间报价需换算为天数存储。尽可能为每个维度都提供报价:没有区间的维度会被静默地视为无法评分,这会导致估算者得到一个仅基于单一维度计算的覆盖率。lo/hi是你实际承诺的区间(P50→P85 范围,或按模式划分的范围)。score.mjs会以 80% 的名义覆盖率对此评分。p50可以为 null,此时score.mjs会使用区间的对数中点 (sqrt(lo*hi)),并标记所有由此推导出的比率——因此推导出的中心值不会被误认为是你报出的数值。task_desc_hash——去除锚点后的描述的 sha256。没有工单时,此字段可选但推荐:它能避免将描述文本写入账本,同时仍可在之后用于匹配。
旧版行(架构变更前的手写格式:agent_h_p50、agent_h_band、
rounds_band、calendar_weeks_band)会在读取时规范化,仍可评分。新行请使用规范格式。
并非每个真实字符串都是有效的关联信号。stitch.mjs 会排除通用分支
(main 出现在所有曾推送过代码的聊天中),出于相同原因,也会排除未经验证的工单键:
在一轮评审中粘贴了带日期的报告文件名,结果生成了 REPORT-2026,把 46 个聊天和 79 个 Agent 工时合并成了一个「任务」。只有当工单键的扇出低于承载阈值、其前缀出现在功能分支中,或其命名空间处于活跃状态时,该键才会成为关联边——也就是说,还有多个同级键会在它之外单独出现,因为一起粘贴的片段总是同时出现,而真正的工单不会。相关规则及其依据见 isPlausibleTicketKey(extract.mjs)和 eligibleTicketKeys(stitch.mjs)中的注释。
有三个驱动程序会遍历整个语料库,且都支持续跑——再次运行时会接着处理,而不会重新开始:backfill.mjs(遍历转录文件根目录 → chats.ndjson + tasks.ndjson)、
classify.mjs(haiku → classifications.ndjson,并合并到 tasks.ndjson)、
audit.mjs(按分层抽样进行 sonnet 复判 → audit.ndjson + 每个字段的一致性)。
模式:estimate
**以下顺序必须遵守。**每一步都是为了避免特定问题。不要调整顺序,也不要跳到后面说「嗯,这看起来大概需要一天」。
1. 移除锚点——在进行任何推理之前
阅读任务描述。在考虑工期之前,先排除以下因素:明示的截止日期、目标日期、故事点、“应该很快/很小/很庞大/很简单”、用户自己的估计、客户承诺的日期、工单中的估算字段。
提示中的无关数字会让估算偏移 4–5×,即使别人告诉你忽略它,或这个数字明显来自不具备相关资格的人,影响依然存在。锚点一旦入眼就无法抹去,所以要明确处理:如果请求中包含锚点,就说 “注意到你提到了约 X——下面的数字源自历史数据,是在阅读该数字之前计算的”,并且绝不让它影响分位数。
2. 就绪度门槛——拒绝估算尚未就绪的工作
如果符合以下任一条件,就不要给出估算:
- 没有可测试的验收标准。 没有人能以测试或具体观察来判定“完成”的标准。
- 实现策略尚未达成一致。 两种或更多实质上不同的方案仍在考虑中,且方案选择会让工作量相差超过一个档位。
这类任务最容易失控:其分布呈双峰——约 78% 的“简单”任务接近其类别中位数,而另外约 22% 的任务会超出 180% 以上;超时任务几乎全都未通过这一门槛。
请改为输出:
目前无法估算——建议进行限时探索。 缺少:
<具体缺失的标准或尚未解决的分歧>。 时间盒:<梯级上的一个档位>,探索的交付物应是缺失的内容,而非功能本身。 探索结束后重新估算;届时任务类别会有所不同,估算范围也会收窄很多。
建议进行探索是一个有效的答复。对未明确的任务给出数字则不是。
3. 提取特征
遵循 prompts/estimate-features.md。这是一份包含实际运行命令的检查清单——执行搜索,不要猜测答案。它会得出:
repos[]、task_type(bugfix|feature|refactor|migration|infra|investigation|script)、greenfield|brownfield、precedent_in_repo(是否曾在此处交付过这种类型的工作——找到先例,或说明没有)、self_verifiable(tests|typecheck-only|human-eyes|prod-only)、blast_radius(可能受影响文件的 grep 计数)、external_deps[](第三方审批、其他人员、部署)、parallelizable(能否跨文件/仓库并行)。
在所有特征中,先例和自我可验证性影响最大。把搜索时间花在这两项上。
然后确定任务所属的加速象限,因为 Agent 带来的折扣并非一个标量:
| 简单 | 复杂 | |
|---|---|---|
| Greenfield | +30–40% | +10–15% |
| Brownfield | +15–20% | 0–10%,有时为负 |
成熟仓库中的大多数工作都落在右下象限,折扣接近于零,甚至可能反转。记录所属象限;第 6 步会用到它。
4. 检索参考类别
按以下顺序从 $LEDGER/ledger/tasks.ndjson 检索:
- 特征向量上最接近的 k = 1–3 个邻居。刻意选用较小的 k——较大的 k 会回归均值,抹去类比法的优势。
- 如果紧密类别的 n < 5,则扩大到整个
task_type类别,并说明已扩大范围。 - 如果样本仍不足,则回退到
calibration.json中的总体先验,并说明估算基于先验。
在依据某个字段作判断前,先检查 field_reliability,并说明检查结果。 其中的每个字段都有 reliable: true 或 reliable: false,且附有实测一致率——没有未测量的字段。截至 2026-09-01 的审计,唯一未达标的是 task_type(原始一致率为 0.38/可采纳一致率为 0.43,n=58 stride),因此,扩大到 task_type 类别是此列表中最不可靠的做法,必须明确说明这一点:优先使用 k=1–3 个邻居;确实扩大范围时,要说明类别标签本身与更可靠的评判者仅约 4/10 次一致。对于衔接良好的行,一致率为 0.51,因此问题主要在于 26% 的行被合并过度,而非标签本身。
始终记录最终采用的类别及其 n。报告邻居之间的方差,不要只报告中心值:1h/1h/9h 和 3h/3h/4h 的中心值相同,但含义截然不同。
当请求涉及更大任务的一部分时,应改用 epic-slice 类别。 calibration.json 包含 reference_classes["epic-slice"](及其自身的 by_task_type),由 slice.mjs 根据聊天次数 ≥6 的每个任务的子交付项构建。若问题是“支付集成中的 webhook 部分需要多久”,而不是“支付集成需要多久”,就应使用此类别——切片会继承史诗任务中已加载的上下文、分支和约定,因此它与 task_type 相同的独立任务并非同一回事。说明引用的是哪一种类别;两者回答的问题不同,而史诗任务行仍是两者共同的记录单位。
如果完全没有可用类别,应使用恒定风险模型为人工轮次轴估算规模,而不是凭感觉估算:Agent 达到 80% 可靠性的时限约为 30 分钟的人类等效工作量,因此 human_rounds ≈ ceil(human_equivalent_minutes / 30)。合理区间:实际会话通常需要 3.3–5.4 次干预。如果仅基于先验的估算落在此区间之外,必须说明理由。
5. 计算经验分位数——绝不拟合,绝不用均值
对该类别的实际值排序,并在对数空间中直接从观测值中取分位数。不要拟合对数正态分布(或任何其他分布),再从曲线上读取分位数:拟合会系统性低估尾部,而这恰恰是所有人真正关心的分布部分。不要引用均值;对于厚尾类别,均值既不代表典型情况,也不代表风险。
引用结果前,先检查自己的偏差。 运行 node $SKILL/scripts/score.mjs 并查看各维度的行:这是关于此估算器本身而非任务的唯一证据。
- 如果输出 TOO EARLY TO CALL(得分配对少于 5 组),请直接引用该类别,并说明估算器本身尚未经过验证。不要根据一两个点应用校正——单个比率只是一次观测,不代表偏差。
- 当它报告的几何平均比率对应 n ≥ 5 时,优先采用 EBS 比率重采样:有放回地抽取完整的历史估算值/实际值比率(抽取 100 次以上),并用每个抽取结果除以该类别中心值。原则相同——使用完整的观测值,进行重采样,绝不取平均。请在记录行的
source中注明,以保留数据来源信息。
样本门槛。每次都要说明当前所处的门槛:
| 类别中的 n | 可引用内容 |
|---|---|
| < 5 | 不得引用该类别的任何结果。仅依据先验,并明确说明。 |
| 5–9 | 仅 P50,并警告目前还没有可用的上界。 |
| 10–19 | P85。 |
| ≥ 20 | 可以使用 P95。 |
当样本量低于约 20 时,类别分位数还会按 w = n/(n+κ) 向总体先验收缩——n=11 时,个人 P85 的范围为真实值的 0.31–2.26 倍,因此它可以作为调整依据,但不能单独使用。calibrate.mjs 会输出收缩后的值;请使用它,不要重新计算原始分位数并引用原始值。
15 个新鲜数据点胜过 150 个陈旧数据点。calibrate.mjs 已应用 90 天半衰期——不要手动重新加权。
6. 内视角调整——最多一个档位
对数阶梯:≈15m · 1h · 3h · 1d · 3d · 1w+。
你最多可以将类别结果移动一个档位,且必须说明具体证据:“本月相同的 Prisma 迁移已上线两次,均在 2h 以内。”没有具体依据的直觉(“感觉更简单”)不构成调整理由。调整两个档位就不是调整,而是换了类别——请返回第 4 步。
然后应用第 3 步的象限折扣——并在应用时扩大区间,绝不能缩小区间。 提速与方差会同步变化:预期收益最大的象限,分布范围也最宽。“Agent 会处理”是扩大区间的理由,而不是缩小区间的理由。
为失败分支预留比成功分支更长的时间。失败轨迹比成功轨迹长 18–82%;失败代价高昂且发生得晚,不是代价低且发生得早。
默认使用类别的 P85。降至 P50 必须说明消除了尾部风险的缓解措施:规格已冻结、测试框架已就绪、没有外部依赖、仓库中有先例。
7. 输出 — SLE,三个维度,绝不是单一数字
**85% of your similar tasks finished in ≤ X agent-hours, ≤ Y human rounds, Z calendar days**
(class: <description>, n=<N>, neighbors: <task ids>)
然后按以下顺序给出:
- P50 进度线 — “一半的任务在 ≤ … 内完成”,让典型情况与上限一目了然。
- 类型判断 — 分布是双峰的,并非围绕中心展开。说明你认为它属于哪种类型,以及原因:agent-nails-it(已有先例、测试可验证、影响范围窄)或 integration-thrash(只能靠人工目测或仅在生产环境验证、没有先例、涉及外部接口)。如果两种情况都可能出现,给出两者及大致权重。单一的混合数字落在两个峰之间的低谷里,几乎毫无描述意义。
- 风险骤增标记,逐项命名,只列适用项:规格含糊 · 只能人工目测验证 · 影响范围广 · 等待外部事项 · 仓库中没有先例。说明标记名称和触发它的具体原因。
- 单独列出外部日程等待时间。应用审核、平台审批、他人的部署窗口、人工决策。这些是空等时间,不是工作——绝不能计入 Agent 工时;这也是日历天数和 Agent 工时分列两个维度的原因。
- 评审等待时间计入日历天数。如果完成意味着合并,估算就不能止于“代码已写完”——这部分才是变快的部分。评审时间增加了 441%,而由 Agent 编写的 PR 等待接手的时间长了 5.3 倍。只要任务以 PR 结束,且必须由他人合并,就要明确计入等待评审的时间。
- **只要估算超过该类别的 P85,就说“拆分此任务”。**合理缩小任务规模,胜过增加预算。提出实际拆分方案,将任务拆成每块 ≤1 个桶的部分,不要只给建议。
绝不只给一个数字。对重尾类别给出点估算,是类别错误;这种格式会让人承诺日期,最后却无法按期完成。
8. 记录估算 — 必须做,而且要立即做
**从未记录的估算永远无法评分。**这一步能让技能持续改进,而不是一错再错;因为用户已经得到答案,它也最容易被跳过。给出估算的同一轮就要完成记录。
在 $LEDGER/ledger/estimates.ndjson 中追加一条 estimate.v1 记录——架构和字段规则见 Ledger layout;三个维度以及 record.mjs 之后能再次找到的 task slug 是其中的关键部分:
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
然后用一句话告诉用户闭环已启动,完成此次估算:“已记录为 2026-09-01-csv-export——任务完成后运行 record 模式,即可自动评分。”
关于该记录中的数字,有两条规则:
- **记录你告诉用户的内容,不要换成更保守的版本。**在记录中悄悄扩大区间,只会制造估算器并未赢得的覆盖率。
- **工作开始后,绝不修改已记录的行。**知道结果后再修改的估算,就不再是估算。另记一行,让两条记录都参与评分。
存储哈希,而非文本——这样 ledger 中不会包含任务描述;之后 record 模式会通过哈希匹配任务,并计算比率(实际值/估算值)和区间覆盖率。
准则 — 质疑估算的形态时,明确说明这一点
时长 ≈ 轮次 ×(每轮 Agent 时间 + 每轮人工等待时间)
人工轮次是最主要的预测指标(实测数据中 r ≈ 0.83)。首轮时长没有预测作用(r ≈ 0.0)。
因此,一项一次就完成、耗时 40 分钟的任务,与一项经历 8 轮“还不太对,再试一次”的任务不可相提并论,无论两者的 diff 看起来多么相似。估算轮次;分钟数自然会随之而来。每轮人工等待时间是用户自身的延迟——它已包含在记录的实际数据中,不要单独建模。
适用时,值得明确指出以下两点:
- 首轮耗时长是好兆头,不是坏兆头。 它预示着轮次更少——前置的背景说明会带来回报。绝不要因为任务一开始进展缓慢就估算它会很慢。
- 等待权限提示是最大的一项可挽回损失。 在较长的任务内停顿中,审批间隔占 52%,每次中位数为 32 分钟。如果用户的配置要求每次调用工具都审批,那么耗费用户一下午的正是这一点,而非模型速度。
估算轮次和步骤,绝不估算行数。助手步骤与时长的相关系数为 r ≈ 0.86,与人工轮次的相关系数为 r ≈ 0.82;LOC 的相关系数仅为 0.53。diff 大小估算的预测能力,比那个看起来更具体的指标还差。
绝不要将自我报告的提速作为校准输入。“那花了我 20 分钟”不是数据;这是感知差异在作祟。只有挖掘出的实际数据才能记入台账。
模式:设置
1. 结构
检查 $LEDGER(解析顺序见 台账布局)。创建 ledger/、空的 chats.ndjson / tasks.ndjson /
estimates.ndjson,以及占位文件 calibration.json。不要覆盖任何现有内容——如果台账已有记录,则应进入重新校准模式,而非设置模式。
2. 同意 — 在接触任何对话记录之前
停止并使用 AskUserQuestion 征求同意。 未得到答复前,不要读取任何对话记录。 明确说明以下内容:
- 它会读取此机器上
~/.claude*/projects/下的 Claude Code 对话 JSONL(两个账户)。 - 除分类调用外,任何内容都不会离开此机器;分类调用会通过
claude -p发送至 Anthropic API——也就是用户常规会话使用的同一位置。 - 存储的内容:任务形态和时长,而非对话文本。
- 此操作可撤销:
rm -rf ~/.claude/estimation。
提供范围选项:所有项目、一个项目,或最近 N 个月。
3. 回填 — 确定性遍历
只需一条命令,即可处理整个 transcripts 根目录——它会遍历每个项目目录,按顺序提取每个
.jsonl(绝不并行:这些文件最大可达 140 MB,而且这是一个正在使用的环境),最后进行全局合并:
nice -n 19 ionice -c3 node --max-old-space-size=2048 $SKILL/scripts/backfill.mjs \
--root ~/.claude/projects
它会将小于 5 KB 的对话记录视为噪声并跳过(同意探测、未能通过横幅提示的会话),同时统计其数量;如果某个对话的文件和 sidecar 目录自上次运行后均未更改,也会跳过。中断后重新运行即可——对话记录会在提取后立即追加。
临时文件和进度日志会放在 /tmp/claude/task-estimator-backfill/ 中,绝不会放进代码仓库,也不会放进账本。
Subagent sidecar 也会被挖掘,而委派的工作就在其中。 在 <chat_id>.jsonl 旁边,harness 会写入 <chat_id>/subagents/agent-*.jsonl,以及一个列出每个 Agent 类型的 .meta.json。extract.mjs 会对每个文件运行同一套提取核心,并将结果汇总到父级对话的 subagents 对象中,包括 loc_added/loc_removed、files_edited、repos、active_min、sidecar_count 和 per_type 分类统计。这不是可有可无的改进:在当前 Claude Code 版本中,Task/Agent tool_result 不包含 toolStats,因此如果没有这些数据,将所有工作都委派出去的对话会显示为 0 LOC;而且父级甚至不会记录每次生成的 Agent(一次生成了 104 个 Agent 的冲刺任务只记录了两个)。以下两条规则绝对适用:
- 绝不将任何内容合并到父级自身字段中。 Subagent 的运行分钟数包含在其父级的总耗时中,若再加上这些分钟数,就会把同一段时间重复计算。对话的
loc_added、files_edited和agent_active_min均不会改变。 subagents内部也绝不重复计数。 如果父级确实报告了某次生成的toolStats,且存在该toolUseId对应的 sidecar,则 sidecar 的测量结果会取代报告值——它才是主要记录。没有 sidecar 的生成操作则保留其报告值。深度为 2 的 Agent(由另一个 subagent 生成,因此父级中完全没有tool_use块)也会计入,这就是为什么subagents.count可能超过生成块数量。
Sidecar 绝不会作为独立对话输出。
每个对话还会有一个 repo_share——格式为 {repo: fraction},各项之和恰好为 1,按照在各仓库中编辑的文件数量,按比例拆分对话的分钟数(包括父级和 subagent 的编辑)。未编辑任何文件的对话,其分钟数全部归入其 cwd 对应的仓库;如果 cwd 是多仓库根目录,则归入 workspace。stitch.mjs 会将其汇总到任务行的 agent_hours_by_repo / subagent_hours_by_repo 中。进行任何按仓库或区域统计时,都应使用这些字段。 对 repos 中的仓库逐一累加 agent_hours,会导致每个多仓库对话在每个仓库中都被计算一次——在这批数据中,这使区域汇总结果膨胀了约 25 倍。
探测会话会带有 noise 标记——值为 "permtest"(权限探测:echo PERMTEST_OK、“testing perms here”)或 "qa-probe"(在 /tmp 临时目录树中运行的会话)。检测依据是比例,而非关键词,因为真实对话可能会长篇讨论权限探测;只有当探测几乎就是会话的全部内容时,才会将其标记为探测会话。这些行仍保留在账本中,calibrate.mjs 会在计算分位数时将其排除。
4. 回填 — 语义处理
每个聊天分类一次:digest.mjs → haiku。运行前,先统计聊天数量,估算 token 开销,打印结果并询问是否继续。 回填一年的转录并非没有成本,用户有权先看到这个数字——--estimate-only 只会准确打印这些信息,不做其他操作。
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 会为每个聊天执行此调用,并设置 MAX_THINKING_TOKENS=0(实测:每次调用耗时 6.5 s,花费 $0.007;而启用 thinking 时耗时 44 s,花费 $0.025,且没有违背提示词规则的损失——审计阶段才是保证分类准确的关键):
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'
每个标志都至关重要:
--tools ""— 不启用工具循环。这是分类器,只读取文本并返回 JSON。--safe-mode— 不加载 CLAUDE.md、技能、钩子、MCP 服务。这样分类器不会受到项目指令的干扰,并保持提示词前缀稳定,让批次能命中提示词缓存。身份验证仍正常工作(不同于--bare)。--no-session-persistence— 至关重要。 如果不使用它,每次分类都会在~/.claude*/projects/中写入新的 JSONL,下一次回填就会把这些文件当成聊天挖掘。账本会开始啃自己的尾巴。--output-format json会返回一个封装对象;模型文本位于.result中,且仍可能带有代码围栏。解析前要去掉 ``` 围栏。也可以使用提示词文件中的 Schema 区块,通过--json-schema加强校验。
将返回的字段与 classified_at 和 classifier_model 一起合并到任务行中。一个任务可能包含多个聊天,因此合并时有以下规则:
- 标签取自主聊天 — 即将自己标记为
main的聊天所给出的分类;如果并列,则按 Agent 活跃分钟数决定。标签不进行投票:对 bug 修复和重构取平均值,得到的结果两者都不是。 outcome根据产物而非分类器判定。 如果任务的commits > 0或prs非空,则一律为shipped——这些字段来自 git 和gh的输出,而非模型对截断摘要的阅读结果;它们恰好符合标准的 完成即合并 规则集。分类器只保留产物无法表达的两种判定:shipped-then-reverted和abandoned(这两种情况都有提交,但随后发生了其他事情)。outcome_source会记录deterministic或classifier,而classifier_outcome会保留原始结果,以便审计时仍能评估模型。这是因为分类器的判断悲观过头,比例达到 2.3:1——一次 sonnet 审计将 62 行从unknown → shipped,反向调整的只有 27 行;这全是“宁可选 unknown,也不要猜测”规则在起作用,因为摘要根本没有显示提交记录。external_waits和user_touchpoint_kinds会汇总所有聊天中的内容。 它们分别是数量和集合,而不是标签——第 3 个聊天中发生的等待,仍然是该任务付出的无效时间。- Epic 保持合并,并保留细节。 多聊天任务仍是一行(拆分任务正是导致账本自我美化的合并不足问题),并通过
sub_deliverables[]数组保留每个聊天各自的标签。单个聊天如果报告了多个工作单元,也会生成多个条目。
分批处理并支持续跑。 分批处理,每批处理后写入,并跳过已有 classified_at 的任务。回填中断后,应能通过重新运行同一命令继续,而不必从头开始。
5. 审计——分类器可信吗?
采用分层抽取五分之一的样本,并对以下情况过采样:规模最大的任务、stitch_confidence 较低的行,以及 haiku 与确定性信号矛盾的行(没有提交却判定为 shipped;有已合并的 PR 却判定为 abandoned)。
使用 sonnet 和 prompts/audit-task.md 重新判断每一行。audit.mjs 会进行分层(完整纳入三个分层,其余行按确定性的 1-in-rate 步长抽取,因此重新运行时审计的是相同行),将任务记录及其聊天摘要提供给评审,并输出各字段的一致率:
node $SKILL/scripts/audit.mjs --plan-only # what would be sampled, and why
node $SKILL/scripts/audit.mjs --rate 5
报告各字段的一致率。如果任何字段的一致率低于 80%,请标记该字段:说明提示词需要修订,并在 calibration.json 中将该字段标记为不可靠,而不是悄悄将它用于特征向量。未经检查的分类器不能作为证据。
请仔细阅读表格中的两点:
- 报告的比率仅针对步长分层。另外三个分层刻意选取最难判断的行,因此其一致率不能代表总体比率。
outcome按账本实际持有的值评分;对于有产物的行,该值是确定性的。这是回答“我能否信任此字段”的正确数值,但不是对分类器的测量——应使用classifier_outcome来评估模型。
只有审计员有权进行的更正才计入结果。有三类更正不予采纳,calibrate.mjs 会在调整后比率中扣除这三类更正——reliable 正是依据该比率判定的,因为不予采纳的更正不应导致字段被判定为不可靠。当这类更正在所有分歧中占多数且至少有两条时,该字段会被报告为 unmeasured,而不是不可靠:这是 prompts/audit-task.md 的缺陷,不是数据问题,因此在相信任一数值之前,应先修复提示词并重新审计。
off_spec_corrections——值超出该字段自身的枚举范围。使用不同词汇体系的“更正”,回答的是另一个问题。corrections_contradicting_deterministic_fields——对于commits/prs来自 git 的行,将outcome更正为unknown。评审最多只能看到六条截断的摘要;记录涵盖全集,摘要只是样本,因此“我没看到提交”不能算作一项判断。self_contradicting_corrections——某项判断为false,但其更正重复了记录中已有的值。它先声称存在分歧,之后却无法指出分歧是什么。
请将 stitch 判定与标签比率一并查看,因为它能解释大部分剩余差异。field_reliability.stitch 会报告判定分布和 over_merged_share,每个字段还会包含 agreement_on_well_stitched_rows——也就是仅在审计员判定为 correct 的行中计算的相同比率。本次测得:在 stitch 判定为 correct 的行中,task_type 的一致率为 0.51;而在被判定为 over-merged 的 26% 行中,一致率为 0.00。十五行全错不是抛硬币般的偶然,而是问题本身没有答案的特征,因为一条合并过度的行包含两个或更多工作单元,因此不存在单一的真实 task_type。这种条件分析是一种诊断手段,刻意不会改变 reliable——账本确实在这些行中保留了错误标签。它告诉你该修复什么:stitch,而不是提示词。
6. 拆分史诗任务
node $SKILL/scripts/slice.mjs --min-chats 6 # add --dry-run --json to look first
包含几十次对话的任务,是恰当的记录单位,却不是适合估算的单位:
没人会问“一个 76 小时的集成要多久”,而是问“Webhook 部分要多久”。
把史诗任务拆成单独的任务行,正是导致过度合并、让账本自我满足的做法,因此任务行永远不会变动。slice.mjs 不会这样做,而是为任务创建一个视图——task.sub_tasks[],先按共用 PR对对话分组,再按分类标签重叠且时间相邻分组(两个条件缺一不可:三周后出现的相同 slug 属于回归,不是同一个切片)。
如果某个切片中的所有对话都报告没有工作单元,则会将其标记为 no_work,并从 epic-slice 分位数中排除;这与从任务分位数中排除 no_work 任务行完全相同,原因也相同——它在每个类别中都处于底部,回答的是另一个问题。它仍会保留在 sub_tasks 中;只有统计数据会忽略它,而 excluded_no_work_slices 会报告其数量。
每次对话都恰好归入一个切片,因此所有切片的小时数和轮数相加后,等于史诗任务本身的数值。正是这一不变量,让切片的估算有据可依,而非主观印象。请在 classify.mjs(读取 sub_deliverables)之后、calibrate.mjs 之前运行此步骤;后者会将切片转换为 epic-slice 参考类别。
7. 校准
node $SKILL/scripts/calibrate.mjs --ledger $LEDGER/ledger --out $LEDGER/calibration.json
两类任务行会从所有分位数中排除: noise(权限探测、临时 QA 会话)和 no_work: true(分类器未找到工作单元——纯问答、状态检查、只进行阅读的会话)。两者都会保留在 tasks.ndjson 中;只有统计数据会忽略它们,而 calibration.json 会在 excluded 中报告排除数量,因此排除情况始终可见。
no_work 排除项是根据证据对先前判断的修正。旧的推理——“花两小时阅读才回答的问题,成本就是两小时”——对小时数来说没错,对类别来说却不对。在这份账本中,这类任务行占 426 条中的 127 条,在每个类别中都集中于底部;而参考类别只有在成员属于同一类事物时才能进行预测。若将它们保留在其中,就会用一个有 30% 概率回答“回答一个问题要多久”的分布,来回答“修复一个 bug 要多久”。问问题,就会得到问题的数字;问 bug 修复,就不该得到这个数字。
对于读取 calibration.json 的任何内容,结论是:n_tasks 是可用于估算的数量,不是账本大小。n_tasks_in_ledger 才是账本大小。
然后打印:每个类别的 n、总体先验、哪些类别已达到 n≥10 的门槛,以及排除了多少行。这样用户就能知道账本目前实际可以回答哪些问题。
8. 提供 CLAUDE.md 钩子
显示 templates/claude-md-snippet.md,展示确切的差异,征求用户同意,然后才将其追加到用户的全局 CLAUDE.md 中,放在其 `` 标记之间。如果标记已存在,则替换该区块,而不是再追加一份。未经事先展示差异,绝不编辑 CLAUDE.md。
模式:record
在实质性任务结束时运行,无论是因为用户说了“记录此任务”,还是因为 CLAUDE.md 片段触发了此操作。
运行 record.mjs。不要手动拼装任务行。 以下规则都已包含在其中,而其中两条最容易手动出错——返工合并和结果来源——恰恰是最容易让账本自我粉饰的两条:
SKILL=.claude/skills/task-estimator
# the session you are in right now:
nice -n 19 ionice -c3 node --max-old-space-size=2048 $SKILL/scripts/record.mjs \
--session <session-uuid> --project <transcript-dir-name>
# or, when the transcript is simply the newest one in that project:
node $SKILL/scripts/record.mjs --latest --project -path-to-your-project
实用标志:--dry-run(打印任务行,不写入任何内容——务必先运行此标志)、--outcome <shipped|shipped-then-reverted|abandoned|superseded|unknown>、--in-progress(工作仍未完成;结果保持为 unknown,并如实注明来源)、--estimate-id <id> 用于指定评分所依据的报价、--ticket <KEY> 用于提供对话记录中从未提及的返工关联键、--json、--no-calibrate。
它的作用,以及为什么每个步骤都不能由你自行发挥:
- 以结构化方式提取并串接。 恢复的会话是同一任务的延续,而不是新任务——串接会沿着父级边进行(文件中第一个
sessionId≠ 文件名),并且绝不会按经过的时间分组。不要按时间手动拆分长任务。聊天记录采用 upsert 方式,因此重复记录同一会话是幂等的:现有任务行会被替换,绝不会重复。 - 返工计入原始行。 如果存在 PR 或工单相同的任务行,且这项工作在14 天内完成,这些行就会合并;合并后的行保留原始
task_id,并在rework_merges[]中记录合并情况。成本会根据聊天记录重新推导,因此文件会取并集,而两行都记录的同一 SHA 只计一次。把后续工作登记为新任务,是最常见的错误做法,会让历史记录在无声无息中看似准确。 done表示已接受或已合并。 Agent 说“完成”会把完成程度夸大约 7 倍。任务行上有提交记录或 PR 时,会设置outcome: shipped和outcome_source: "deterministic",且--outcome无法覆盖它——只有shipped-then-reverted和abandoned这两种结果可以保留在有产物的情况下,因为产物无法表达这两种判定。没有产物也没有回答时,结果为unknown,绝不会乐观地设为默认值。- 只询问无法从记录中提取的信息,且最多问几个问题:
- 如果没有已合并的 PR 可供读取结果,则询问结果(
--outcome); - 外部等待——哪些时间属于无效等待,以及大约持续了多久。 其他所有信息(轮次、跨度、修改的文件、仓库)都来自对话记录。不要询问用户花了多久;账本的存在,正是为了取代这种自我报告。
- 如果没有已合并的 PR 可供读取结果,则询问结果(
- 匹配到报价时会自动评分(
estimate_id→ 工单 →task_desc_hash→ slug → 摘要 → 子交付项)。它会将比率和各维度的区间内判定写入估算行,并在任务行上标记estimate_id,避免再次猜测关联关系。如果它报告“没有匹配的报价”,请告知用户——这说明闭环尚未完成,并非无关紧要的细节。可能是从未记录估算(请修复估算模式的第 8 步),也可能是 slug 不匹配;可使用--estimate-id手动关联。 - 会重新切分 Epic,并自动重新运行
calibrate.mjs。 每完成一个任务,该类别就多一个数据点,下一次估算应立即反映这一点。向现有 Epic 记录聊天也会更改其sub_tasks,因此会在同一次写入中重新切分——过时的切片列表会给epic-slice类别提供已不再符合账本的工时。
然后向用户复述一段话的摘要,包括未测量的内容(未检测到仓库、没有提交记录、结果来源于用户而非产物)。如果报价获得了评分,最后运行 score.mjs,以便报价及其判定一并写入。
模式:重新校准
对整个台账重新运行 stitch.mjs、slice.mjs,然后运行 calibrate.mjs,接着运行 score.mjs——它会计算下面的每个数字,并拒绝计算台账尚未达到条件的指标:
node $SKILL/scripts/slice.mjs --min-chats 6 # refresh the epic sub_deliverables first
node $SKILL/scripts/calibrate.mjs --ledger $LEDGER/ledger --out $LEDGER/calibration.json
node $SKILL/scripts/score.mjs # add --json for the machine-readable form
根据其输出报告:
- 区间覆盖率——在有实际值的报价中,有多少比例落在报价的 P80 区间内?应约为 80%。低于这个值说明报价过于紧凑;明显高于则说明区间过宽。
score.mjs会在旁边打印当前样本数 n 对应的二项分布接受区间;请一并报告该区间,绝不可不提它就判定结果不达标。n=1 时,接受区间为 0–100%。 - 有符号偏差——log(实际值 / 报价 P50)的中位数,并同时报告几何平均比率,这样高估和低估就不会相互抵消。绝不要对比率做线性平均:4× 和 0.25× 是方向相反、程度相同的误差,而它们的算术平均值(2.1×)会凭空捏造出偏差。
- WIS-lite 分解,适用于 n ≥ 5——将 80% 区间得分拆分为区间紧度 + 过高预测 + 过低预测。观察各项占比:区间紧度占主导说明区间过宽,过低预测占主导说明报价偏低。低于 n = 5 时,该指标不会提供;诚实的报告方式是说明“未提供”,不要用其他数字替代。
- 仍在等待实际值的报价——每一项都是已完成、但运行时未启用 record 模式的任务。请列出它们;这就是流程中的漏洞。
- 各类别的 n,以及自上次以来哪些类别达到了 5 / 10 / 20 的门槛。
- 任何变化:刚刚达到可报价条件的类别、覆盖率骤降的类别。
时效性(90 天半衰期)已纳入 calibrate.mjs。不要在此基础上再次衰减。
流程规范——如何在运行中的机器上运行这些脚本
这个语料库包含数百 MB 的转录文本,而机器同时还在为用户自己的会话提供服务。这里的每条规则都是用代价换来的。
- 绝不要用
pgrep、pkill -0、ps | grep或任何基于它们的循环等待进程。pgrep -f node、pgrep -f backfill等命令会匹配检查进程本身——运行检查的 shell、子 shell、grep 进程——因此循环会永远观察到匹配项,永不退出。已有两个 Agent 在这个语料库上因此死锁。 不存在安全的标志组合;也不要尝试使用pgrep -v $$。- 等待自己启动的进程:让它在前台运行,或者记录其 PID,并且只等待该 PID(对于子进程,使用
wait <pid>;单次检查存活状态可使用kill -0 <pid>,但不要放在循环里)。 - 协调不同的运行:使用 PID 文件。
record.mjs会将自身的 pid 写入$LEDGER/ledger/.record.lock;如果锁仍由存活的进程持有,它会立即拒绝运行(绝不会轮询),如果记录的 pid 已不存在,则接管锁。--force-lock可覆盖此行为。因此,终止运行不会卡住台账,而且永远不会扫描进程表。 - 使用单次、不循环的
ps快照来回答“当前是否正在运行回填任务”没有问题。害你出问题的是循环,不是工具。
- 等待自己启动的进程:让它在前台运行,或者记录其 PID,并且只等待该 PID(对于子进程,使用
- 对任何遍历语料库的操作使用
nice -n 19 ionice -c3(backfill.mjs、classify.mjs、audit.mjs,以及处理大型转录文本时的record.mjs)。用户正在这台机器上工作。 - 限制堆内存:
node --max-old-space-size=2048。 单个转录文本可能达到 140 MB;不设限的运行可能会占满整台机器。 - 绝不要并行化提取过程。 文件会按顺序处理,一次处理一份大型转录文本,这是有意为之。
- 临时文件放在
/tmp/claude/task-estimator*/,绝不要放在代码仓库或台账中。任何会重写台账的运行开始前,都要先在那里备份台账。 - 台账写入是原子操作(临时文件 + 重命名),这样并发读取者就不会看到只写了一半的 NDJSON。请保持这一特性——使用
lib.mjs中的writeNdjsonAtomic。
为什么制定这些规则
简而言之。来源和完整说明见 references/。
- METR RCT 2025(16 名专家开发者,在其维护的代码仓库中完成 246 项真实任务)——使用 AI 时速度慢了 19%,却认为自己快了 20%;两者之间存在 39 个百分点的认知差距,且不会随经验积累而自行纠正。对大型成熟代码仓库中的专家来说,这一差距最大——而这里恰好就是这种情况。因此,禁止将自我报告用作校准输入,并从工作记录中提取数据,而不是向人询问。
- 完成度膨胀约 7 倍——按维护者审核标准衡量,Agent 自行宣称的完成度会夸大实际完成度,每次“通过”还掩盖了约 26 分钟的修复工作。只有合并或获接纳才算完成。其他情况一律不算。
- 看轮次,不看分钟——人工轮次数与总时长的相关系数约为 r ≈ 0.83(在包含 651 个会话的语料中为 0.82;助手步骤的相关性更高,达 0.86);相比之下,首轮时长的相关系数约为 r ≈ 0.0,代码行数只有 0.53。每轮 Agent 工作时长稳定在每步约 9 秒,中位数轮次时长接近 45 秒;所有变异都来自轮次数。
- 78 / 22 双峰分布——78% 的高复杂度任务耗时不到预期的 25%,而 22% 的低复杂度任务耗时超过预期的 180%,原因在于验证和集成,而非编码。联合分布呈双峰,因此单一中心值会落在两峰之间的谷底。就任务属于哪种模式而言,就绪度门槛是成本最低的筛选方式:工作量激增与缺少或无法测试的验收标准相关。
- 锚定效应(Løhre & Jørgensen)——无关数字会让估算值改变 4–5 倍,即使明确要求忽略该数字,这种影响仍会持续;即使来源明显不合格,影响也依然存在。因此,必须在任何推理开始前剔除锚点,而不是事后再将其作为 caveat。同一个人对同一任务重新估算,偏差可达 71%,所以任何记忆中的估算值都不能作为数据。
- 参考类别预测(Kahneman/Lovallo/Flyvbjerg)——先采用外部视角,再在有限范围内按一个类别调整内部视角。软件项目的类别基准是平均超支 73%,其中 18% 的长尾项目平均超支 +447%:这是厚尾分布,因此默认采用 P85,有外部依赖时采用 P95。
- 基于证据的排期(Spolsky)——对完整的估算/实际耗时比值进行重采样;绝不对速度取平均,因为平均值会抹去导致超支的长尾样本。返工应计回原始事项——单独登记返工正是历史记录悄然显得准确的原因。
- 样本量门槛(Vacanti)——中位数需要 5 个样本;约 11 个样本后,观测范围才有意义;发布上限需要 10–20 个样本;15 个新样本胜过 150 个旧样本。当 n=11 时,个人 P85 的范围为真实值的 0.31–2.26 倍,因此 n≈20 以下的所有数据都要向总体先验收缩。
- 使用经验分位数,而非拟合值——拟合的对数正态分布给出的中位数仅为真实中位数的 ×0.72,p95 则为真实值的 ×1.72;而原始经验分位数的留一法覆盖率分别为 49.9 / 79.8 / 89.9,对应名义覆盖率 50 / 80 / 90——基本完全吻合。对实际值排序,并在对数空间中读取分位数。
- 只有 35% 的任务耗时落在中位数的 2 倍范围内。在这种离散程度下,给出点估算站不住脚;答案应是一个区间。
- 约束已转移到下游——任务吞吐量 +33.7% 的同时,审核时间 +441%,接手时间 +156%,返工率 +861%,并且 38% 的 Agent 生成 PR 因“从未审核”而失败。若估算止于“代码已写好”,衡量的只是提速的那一部分。此外,编码只占开发者日程时间的约 30–40%,因此任何声称端到端提升超过约 40% 的结果都是测量错误,而非真实成果。