@haiyangbg/buildbeat 4.2.0 → 4.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,8 +2,22 @@
2
2
 
3
3
  > 本项目吃自己的狗粮(红线④:必更 CHANGELOG)。格式循 Keep a Changelog,倒序。
4
4
 
5
+ ## v4.2.2 — 2026-10-07(补丁:README 重新设计)
6
+
7
+ - README (zh/en) redesigned for reading: a hero picture, the name's meaning (every step is a note the agent plays; the played score stays in Git), a delivery-loop diagram in light and dark with a stacked layout for phones, the three promises as short sections, a real `status` excerpt at the merge decision, and which records travel with Git and which stay on this machine. Pictures live in `docs/assets/readme/` (the loop diagram is drawn by `diagrams.mjs`) and are linked through raw URLs so the npm page shows them too; `check:docs` now requires every README picture to exist, carry alt text and stay under 600 KB.
8
+
9
+ ## v4.2.1 — 2026-10-07(补丁:审查没交报告当场停;成本按去向分开等待)
10
+
11
+ > **发布状态**:`@haiyangbg/buildbeat@4.2.1` 已于 2026-10-07 从 `main`(release PR #79,merge commit `8d2625b`,tag `v4.2.1`)经 OIDC Trusted Publishing 发布到 dist-tag **`latest`**(run 37626818833,publish 与 verify 双 job 一次 success;所有者授权「推送并发 4.2.1」)。独立回读(直连 npmjs.org):`latest` = 4.2.1、`dist.integrity` 与本地用 Node 24 + npm 11.19.0 打的候选逐字一致、注册表 tarball 解压后与候选逐字节相同、SLSA v1 provenance、隔离安装 `--version` = 4.2.1、裸调用零写入、`npm audit signatures` 通过;GitHub Release v4.2.1 标 Latest,仓库 About 同日更新;证据见 [`docs/releases/V4.2.1-RELEASE-EVIDENCE-2026-10-07.md`](docs/releases/V4.2.1-RELEASE-EVIDENCE-2026-10-07.md)。`next` 仍为 4.2.0。
12
+
13
+ - A review step that exits 0 without writing a report to `$BUILDBEAT_OUTPUT` now stops as `invalid-output` (an infrastructure failure: no fixer, no charge, `resume-review` waits for a person) instead of passing with no evidence until the merge approval refused it. The stop reason says where the report belongs and, when the reviewer printed something, how many bytes went to stdout. Found in a replay of real runs, where a reviewer's 3 P1, 6 P2 and 4 P3 findings reached stdout only.
14
+ - The cost line splits human waits by how they ended: `human waits N (decided … · superseded … · stopped … · open …)`, listing only kinds that occurred; a request repeated before any decision continues the same wait, so overlapping time is not counted twice; JSON carries `cost.waits` with `{count, ms}` per kind and run-records keep it. In the same replay one work showed 32 minutes of decided waits while 11 hours passed on a request a new run superseded.
15
+ - README (zh/en) describes the 4.x product: the three promises now name the mechanisms behind them (one `work.md` driven by `buildbeat run` and `status --all-repos`; evidence read back from Git and infrastructure stops instead of code fixes; merge approvals bound to the candidate and repairs at the merge decision), the cost line's waits, taking part without the runtime and the release readback.
16
+
5
17
  ## v4.2.0 — 2026-10-07(次版本:合并决定点能接修复;合并批准绑定候选)
6
18
 
19
+ > **发布状态**:`@haiyangbg/buildbeat@4.2.0` 已于 2026-10-07 从 `main`(release PR #76,merge commit `cc52ad3`,tag `v4.2.0`)经 OIDC Trusted Publishing 发布到 dist-tag **`latest`**(run 37597691057,publish 与 verify 双 job 一次 success;所有者授权「推送并发 4.2.0」)。独立回读(直连 npmjs.org):`latest` = 4.2.0、`dist.integrity` 与本地用 Node 24 + npm 11.19.0 打的候选逐字一致、注册表 tarball 解压后与候选逐字节相同、SLSA v1 provenance、隔离安装 `--version` = 4.2.0、裸调用零写入、用法含 `--candidate <sha>` 与 `--action fix`、`npm audit signatures` 通过;GitHub Release v4.2.0 标 Latest;证据见 [`docs/releases/V4.2.0-RELEASE-EVIDENCE-2026-10-07.md`](docs/releases/V4.2.0-RELEASE-EVIDENCE-2026-10-07.md)。合并后 `main` CI 第 1 次尝试因跨仓测试快照混入 git 后台维护的锁文件失败、重跑转绿后才批准发布,测试修复随收尾合入。同日所有者本人经 npm 两步验证把 dist-tag `next` 从 4.0.0 挪到 4.2.0;直连 npmjs.org 回读 `next` = `latest` = 4.2.0。
20
+
7
21
  - Bind merge approval replies to `--candidate <sha>` and forward the binding through the CLI; reject old or unbound replies after repair. Support both repair paths when the current candidate is still the base commit. Isolate repair regression fixtures from host Git configuration, signing and hooks. The owner confirmed the public name `--candidate <sha>` and runtime 4.2 as the next minor on 2026-10-07; merge-decision `run --adopt`, `decide --action fix` and candidate binding require runtime 4.2 or later.
8
22
  - Allow manual repair adoption at the merge decision: require clean actual HEAD, a new descendant of the candidate and changes within `allowedPaths`; resume at verify, review incrementally and return to the merge decision while preserving evidence and review accounting.
9
23
  - Add `decide --action fix --reason <text>` at the merge decision when a fixer is configured. Record an accepted P1 with the human's reason, feed it to the fixer and continue through verify/review without spending a review round for the decision or changing budgets. Status, inbox and notifications offer manual repair and the configured fixer path. SUCCEEDED and legacy 3.x Runs retain their existing boundaries.
package/README.en.md CHANGED
@@ -1,16 +1,51 @@
1
- # BuildBeat
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/HaiYangBG1/BuildBeat/main/docs/assets/readme/hero.jpg" width="100%" alt="Five notes on a musical staff, build, verify, review, fix and decide, joined by a pulse line that ends in a stack of written sheet music">
3
+ </p>
2
4
 
3
- **Switch sessions. Keep building.**
5
+ <h1 align="center">BuildBeat</h1>
4
6
 
5
- BuildBeat keeps work context in project files, drives a recoverable delivery loop, and brings evidence to human decisions. It is for humans and AI sessions.
7
+ <p align="center"><strong>Switch sessions. Keep building.</strong></p>
8
+
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@haiyangbg/buildbeat"><img alt="npm version" src="https://img.shields.io/npm/v/@haiyangbg/buildbeat?color=d97706&amp;label=npm"></a>
11
+ <a href="https://github.com/HaiYangBG1/BuildBeat/actions/workflows/ci.yml"><img alt="CI status" src="https://github.com/HaiYangBG1/BuildBeat/actions/workflows/ci.yml/badge.svg"></a>
12
+ <a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-e5534b"></a>
13
+ </p>
14
+
15
+ <p align="center"><a href="README.md">中文</a> · English</p>
16
+
17
+ > **Why the name**: every step is a note, and the agent plays them beat by beat; the score it has played (context, decisions and outcomes) stays in Git, ready to read again.
18
+
19
+ BuildBeat is for humans and AI sessions: the work description, decisions and each run's outcome stay in project files and Git; implementation, verification, review and repair move forward outside the session and can always resume; the candidate waits with its evidence for your merge decision.
20
+
21
+ ## At a glance
22
+
23
+ <picture>
24
+ <source media="(max-width: 600px) and (prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/HaiYangBG1/BuildBeat/main/docs/assets/readme/loop-en-dark-narrow.png">
25
+ <source media="(max-width: 600px)" srcset="https://raw.githubusercontent.com/HaiYangBG1/BuildBeat/main/docs/assets/readme/loop-en-light-narrow.png">
26
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/HaiYangBG1/BuildBeat/main/docs/assets/readme/loop-en-dark.png">
27
+ <img src="https://raw.githubusercontent.com/HaiYangBG1/BuildBeat/main/docs/assets/readme/loop-en-light.png" width="100%" alt="The delivery loop: after work.md come build, verify and review; findings go to fix, verify and review again; then the run stops for you to decide; work.md, decisions, findings and each run's final record stay in Git">
28
+ </picture>
29
+
30
+ Each work gets one `work.md`. Once you accept it, `buildbeat run` moves it forward beat by beat in an isolated worktree: build, run the project's own verification commands, an independent read-only review, and on findings fix, verify and review again. It stops when a person is really needed: the merge decision, an infrastructure failure, an exhausted budget, a review that does not converge. The work description, decisions, review findings and each run's final record go into Git, so a new session, another tool or another person continues from the same record; the live ledger and raw logs stay on this machine (see Records and handoffs below).
6
31
 
7
32
  ## Three promises
8
33
 
9
- Switch sessions without losing progress; let the loop implement, verify, review and repair; inspect the candidate and evidence before deciding.
34
+ ### Walk away
35
+
36
+ **Close the session, switch AI tools or hand over, and the work carries on.** The work description and the run ledger live in the project, and `buildbeat run` resumes from them; in a main repository with several code repositories, `buildbeat status --repo . --all-repos` shows what is pending everywhere.
37
+
38
+ ### Come back to facts
39
+
40
+ **What you find is evidence, not what an AI says about itself.** Verification runs the project's real commands and the runtime reads results back from Git and the ledger; review is independent and read-only; missing tools, an unavailable backend or a review that hands in no report stop as infrastructure failures instead of being "fixed" as code.
41
+
42
+ ### Decide with confidence
10
43
 
11
- ## Start
44
+ **What you approve is the version you reviewed.** A merge approval is bound to the candidate (`--candidate`); a small problem spotted at the merge decision can be fixed and handed over (`run --adopt`) or sent back to the fixer in one line (`decide --action fix`); merge, push, release and deployment stay with people.
12
45
 
13
- The runtime needs Node ≥ 20. Before upgrading from 3.x, finish or cancel runs still in progress with their original 3.x runtime (see [Migration](docs/MIGRATION.md)). Read [SKILL.md](SKILL.md) and the [quickstart](docs/v2/guide/01-quickstart.en.md).
46
+ ## Start in five minutes
47
+
48
+ The runtime needs Node ≥ 20. Before upgrading from 3.x, finish or cancel runs still in progress with their original 3.x runtime (see [Migration](docs/MIGRATION.md)). Read [SKILL.md](SKILL.md) and the [quickstart](docs/v2/guide/01-quickstart.en.md): put the templates in your project, write a `work.md`, and fill in the real AI tool and verification commands in the run config.
14
49
 
15
50
  ```bash
16
51
  npm view @haiyangbg/buildbeat@latest version
@@ -24,15 +59,42 @@ buildbeat run --config delivery/work/WORK-X/run-config.yaml
24
59
  buildbeat status --repo . --work WORK-X
25
60
  ```
26
61
 
62
+ ## What it looks like
63
+
64
+ The first review round of a work found one problem; the fixer repaired it, verify ran again, the second review passed, and the run now waits for your merge decision. An excerpt of real output (`…` marks omissions):
65
+
66
+ ```text
67
+ $ buildbeat status --repo . --work WORK-CSV-EXPORT
68
+ WORK-CSV-EXPORT MERGE_DECISION
69
+ [P1 0b5e258117cb5602] (accept) csv: quote fields that contain commas
70
+ work.md ✓ · runs 1
71
+ cost: review rounds 2 · findings 1 · human waits 1 (open 33s) · worker 2s
72
+ work WORK-CSV-EXPORT:
73
+ RUN-CSV-EXPORT-01 [final-decision] enter-wait-merge — waiting 33s
74
+ candidate: ede20b668cb04ae30cba1676b5e031930e711c8d
75
+ next: buildbeat decide --action approve … --candidate ede20b6… --by <you>
76
+ next: buildbeat decide --action reject … --reason <why> --by <you>
77
+ next: buildbeat run … --adopt <sha> --by <you> # commit a manual repair in the run worktree …
78
+ next: buildbeat decide … --action fix --reason <what to repair> --by <you>
79
+ ```
80
+
81
+ The cost line shows how many review rounds ran, how many problems were found, how long people waited and where those waits went. The decision card offers four choices: approve (bound to this candidate), reject, hand over your own fix, or send it back to the fixer.
82
+
27
83
  ## Records and handoffs
28
84
 
29
- End-to-end work packages contain work.md, configuration, decisions and terminal records under delivery/work/. Local in-flight events and logs live in .buildbeat/runtime/; candidates live in .buildbeat/worktrees/. Save missing facts before switching sessions. Active runs do not migrate through Git clone.
85
+ End-to-end work packages keep the whole story of a work in the repository:
86
+
87
+ - `delivery/work/<ID>/` **travels with Git**: work.md, run config, decisions, review findings, each run's terminal record (outcome, cost, evidence digests and references)
88
+ - `.buildbeat/runtime/` **stays on this machine**: the run ledger, logs, screenshots and other raw evidence
89
+ - `.buildbeat/worktrees/` **stays on this machine**: the isolated worktrees that hold candidates
30
90
 
31
- [Handoff](docs/v2/guide/11-session-handoff.en.md) · [Recovery](docs/v2/guide/10-recovery.en.md)
91
+ Save missing facts before switching sessions; active runs do not migrate through Git clone. People and tools without the runtime can still read status and write work descriptions.
92
+
93
+ [Session handoff](docs/v2/guide/11-session-handoff.en.md) · [Recovery](docs/v2/guide/10-recovery.en.md) · [Without the runtime](docs/v2/guide/12-without-runtime.en.md)
32
94
 
33
95
  ## Boundaries
34
96
 
35
- BuildBeat does not provide multi-user accounts, roles and permissions. It does not collect or upload project usage data and has no telemetry collection. Models and authentication belong to the configured AI tool. Merge, push, deployment and publication remain separately authorized external actions. Production monitoring, release lanes and arbitrary workflow/policy languages are outside the current product.
97
+ BuildBeat does not provide multi-user accounts, roles and permissions. It does not collect or upload project usage data and has no telemetry collection. Models and authentication belong to the configured AI tool. Merge, push, deployment and publication remain separately authorized external actions; after a release, `buildbeat release` records the project's own readback and the window closes only when it passes. Production monitoring and arbitrary workflow/policy languages are outside the product.
36
98
 
37
99
  [Capabilities](docs/CAPABILITY-MATRIX.md) · [Security](docs/v2/guide/09-security-boundaries.en.md) · [Migration](docs/MIGRATION.md)
38
100
 
@@ -46,4 +108,13 @@ The plugin supplies Skill, templates and docs; the runtime is installed separate
46
108
  /buildbeat:buildbeat
47
109
  ```
48
110
 
49
- [Contributing](CONTRIBUTING.md) · [MIT](LICENSE)
111
+ ## Read more
112
+
113
+ | To do this | Read |
114
+ |---|---|
115
+ | Set up a project for the first time | [Quickstart](docs/v2/guide/01-quickstart.en.md) |
116
+ | Talk to a session day to day | [How to talk](docs/v2/guide/00-how-to-talk.en.md) |
117
+ | Understand the fixed flow and the run config | [Workflow and configuration](docs/v2/guide/02-workflow-guide.en.md) |
118
+ | Approve, reject, read evidence | [Decisions and evidence](docs/v2/guide/07-approval-guide.en.md) |
119
+ | Hand over across sessions and people | [Session handoff](docs/v2/guide/11-session-handoff.en.md) |
120
+ | Contribute | [Contributing](CONTRIBUTING.md) · [MIT license](LICENSE) |
package/README.md CHANGED
@@ -1,16 +1,51 @@
1
- # BuildBeat
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/HaiYangBG1/BuildBeat/main/docs/assets/readme/hero.jpg" width="100%" alt="五线谱上依次排着实现、验证、审查、修复、拍板五个音符,一条脉搏线把它们连起来,末端收进一叠写满音符的乐谱">
3
+ </p>
2
4
 
3
- **会话随时换,项目接着干。**
5
+ <h1 align="center">BuildBeat</h1>
4
6
 
5
- BuildBeat 面向人和 AI 会话,把工作上下文留在项目文件中,自动推进可恢复的交付循环,带证据交给人决定。
7
+ <p align="center"><strong>会话随时换,项目接着干。</strong></p>
8
+
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@haiyangbg/buildbeat"><img alt="npm 版本" src="https://img.shields.io/npm/v/@haiyangbg/buildbeat?color=d97706&amp;label=npm"></a>
11
+ <a href="https://github.com/HaiYangBG1/BuildBeat/actions/workflows/ci.yml"><img alt="CI 状态" src="https://github.com/HaiYangBG1/BuildBeat/actions/workflows/ci.yml/badge.svg"></a>
12
+ <a href="LICENSE"><img alt="MIT 许可证" src="https://img.shields.io/badge/license-MIT-e5534b"></a>
13
+ </p>
14
+
15
+ <p align="center">中文 · <a href="README.en.md">English</a></p>
16
+
17
+ > **名字的由来**:每一步是一个音符,Agent 按节拍把它们演奏出来;演奏过的乐谱——上下文、决定和结论——留在 Git 里,随时翻阅。
18
+
19
+ BuildBeat 面向人和 AI 会话:工作说明、决定和每轮结论留在项目文件与 Git 里,实现、验证、审查和修复在会话之外自动推进、随时可恢复,候选带着证据停在你的合并决定前。
20
+
21
+ ## 一眼看懂
22
+
23
+ <picture>
24
+ <source media="(max-width: 600px) and (prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/HaiYangBG1/BuildBeat/main/docs/assets/readme/loop-zh-dark-narrow.png">
25
+ <source media="(max-width: 600px)" srcset="https://raw.githubusercontent.com/HaiYangBG1/BuildBeat/main/docs/assets/readme/loop-zh-light-narrow.png">
26
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/HaiYangBG1/BuildBeat/main/docs/assets/readme/loop-zh-dark.png">
27
+ <img src="https://raw.githubusercontent.com/HaiYangBG1/BuildBeat/main/docs/assets/readme/loop-zh-light.png" width="100%" alt="交付循环:work.md 之后依次是实现、验证、审查,审查有问题就修复、重验、再审,最后停下等你拍板;work.md、决定、审查问题和每轮运行的终态记录留在 Git 里">
28
+ </picture>
29
+
30
+ 一项工作写一份 `work.md`。你接受之后,`buildbeat run` 在隔离的工作树里按节拍推进:实现、跑项目自己的验证命令、独立只读审查,有问题就修复、重验、再审。真正需要人的时候它会停下:合并拍板、环境故障、预算到顶、审查不收敛。工作说明、决定、审查问题和每轮运行的终态记录都进 Git,换会话、换工具、换人都从同一份记录接着干;运行中的台账和原始日志留在本机(见下文「记录与接续」)。
6
31
 
7
32
  ## 三个承诺
8
33
 
9
- 换会话能接着干;实现、验证、审查和修复自动推进;候选和证据就绪后再做决定。
34
+ ### 走得开
35
+
36
+ **关掉会话、换 AI 工具、换人,工作照样接着干。** 工作说明和运行台账都在项目里,`buildbeat run` 从记录恢复;主仓带多个代码仓时,`buildbeat status --repo . --all-repos` 一条命令看全所有仓的待办。
37
+
38
+ ### 回来能信
39
+
40
+ **回来看到的是事实,不是 AI 的自述。** 验证跑项目真实命令,结果由运行时从 Git 和台账回读;审查独立、只读;缺工具、后端不可用、审查没交报告都按环境故障停下,不会当成代码问题去修。
41
+
42
+ ### 敢拍板
10
43
 
11
- ## 开始使用
44
+ **你批准的就是你看过的那一版。** 合并批准绑定候选(`--candidate`);合并决定点发现小问题,可以自己修好交回(`run --adopt`),或一句话退回给修复者(`decide --action fix`);合并、推送、发布、部署始终由人决定。
12
45
 
13
- 运行时需要 Node ≥ 20。从 3.x 升级前,先用原来的 3.x 运行时完成或取消仍在进行的 Run(见[迁移说明](docs/MIGRATION.md))。先读 [SKILL.md](SKILL.md) 与[快速开始](docs/v2/guide/01-quickstart.md)。
46
+ ## 五分钟开始
47
+
48
+ 运行时需要 Node ≥ 20。从 3.x 升级前,先用原来的 3.x 运行时完成或取消仍在进行的 Run(见[迁移说明](docs/MIGRATION.md))。先读 [SKILL.md](SKILL.md) 与[快速开始](docs/v2/guide/01-quickstart.md):把模板放进项目,写一份 `work.md`,在 run 配置里填好真实的 AI 工具和验证命令。
14
49
 
15
50
  ```bash
16
51
  npm view @haiyangbg/buildbeat@latest version
@@ -24,17 +59,44 @@ buildbeat run --config delivery/work/WORK-X/run-config.yaml
24
59
  buildbeat status --repo . --work WORK-X
25
60
  ```
26
61
 
62
+ ## 跑起来是什么样
63
+
64
+ 一项工作在第 1 轮审查里被挑出一个问题,修复者修好、重验,第 2 轮审查通过,然后停在你的合并决定前。下面是真实输出的节选(`…` 处有省略):
65
+
66
+ ```text
67
+ $ buildbeat status --repo . --work WORK-CSV-EXPORT
68
+ WORK-CSV-EXPORT MERGE_DECISION
69
+ [P1 0b5e258117cb5602] (accept) csv: quote fields that contain commas
70
+ work.md ✓ · runs 1
71
+ cost: review rounds 2 · findings 1 · human waits 1 (open 33s) · worker 2s
72
+ work WORK-CSV-EXPORT:
73
+ RUN-CSV-EXPORT-01 [final-decision] enter-wait-merge — waiting 33s
74
+ candidate: ede20b668cb04ae30cba1676b5e031930e711c8d
75
+ next: buildbeat decide --action approve … --candidate ede20b6… --by <you>
76
+ next: buildbeat decide --action reject … --reason <why> --by <you>
77
+ next: buildbeat run … --adopt <sha> --by <you> # commit a manual repair in the run worktree …
78
+ next: buildbeat decide … --action fix --reason <what to repair> --by <you>
79
+ ```
80
+
81
+ 成本行告诉你审查了几轮、发现了几个问题、人等了多久,以及这些等待的去向。决定卡给出四种选择:批准(绑定这一版候选)、拒绝、自己修好交回、退回给修复者。
82
+
27
83
  ## 记录与接续
28
84
 
29
- 端到端工作包在 delivery/work/ 保存 work.md、配置、决定与终态记录;本机在途事件和日志在 .buildbeat/runtime/,候选在 .buildbeat/worktrees/。换会话前补齐未落盘事实。Git clone 不迁移活动进程和现场。
85
+ 端到端工作包把一项工作的来龙去脉留在仓库里:
86
+
87
+ - `delivery/work/<ID>/` **随 Git 走**:work.md、run 配置、决定、审查问题、每轮终态记录(结果、成本、证据摘要与引用)
88
+ - `.buildbeat/runtime/` **只在本机**:运行台账、日志、截图等原始证据
89
+ - `.buildbeat/worktrees/` **只在本机**:候选所在的隔离工作树
30
90
 
31
- [Handoff](docs/v2/guide/11-session-handoff.md) · [Recovery](docs/v2/guide/10-recovery.md)
91
+ 换会话前补齐未落盘事实;Git clone 不迁移活动进程和现场。没装运行时的人和工具也能读状态、写工作说明。
92
+
93
+ [跨会话接续](docs/v2/guide/11-session-handoff.md) · [中断恢复](docs/v2/guide/10-recovery.md) · [没有运行时也能参与](docs/v2/guide/12-without-runtime.md)
32
94
 
33
95
  ## 能力边界
34
96
 
35
- BuildBeat 不提供多人账号、角色/权限系统,不采集或上传项目使用数据,没有遥测采集。模型和鉴权由配置的 AI 工具提供。合并、推送、部署、发布分别由获授权的外部操作完成。生产巡检、上线专用流程和任意工作流/规则语言已移出产品。
97
+ BuildBeat 不提供多人账号、角色/权限系统,不采集或上传项目使用数据,没有遥测采集。模型和鉴权由配置的 AI 工具提供。合并、推送、部署、发布分别由获授权的外部操作完成;上线后可用 `buildbeat release` 记录项目自己的回读,回读通过才关窗。生产巡检和任意工作流/规则语言不在产品范围内。
36
98
 
37
- [Capabilities](docs/CAPABILITY-MATRIX.md) · [Security](docs/v2/guide/09-security-boundaries.md) · [Migration](docs/MIGRATION.md)
99
+ [能力矩阵](docs/CAPABILITY-MATRIX.md) · [安全边界](docs/v2/guide/09-security-boundaries.md) · [迁移说明](docs/MIGRATION.md)
38
100
 
39
101
  ## Claude Code 插件
40
102
 
@@ -46,4 +108,13 @@ BuildBeat 不提供多人账号、角色/权限系统,不采集或上传项目
46
108
  /buildbeat:buildbeat
47
109
  ```
48
110
 
49
- [Contributing](CONTRIBUTING.md) · [MIT](LICENSE)
111
+ ## 继续阅读
112
+
113
+ | 想做什么 | 看哪里 |
114
+ |---|---|
115
+ | 第一次接入一个项目 | [快速开始](docs/v2/guide/01-quickstart.md) |
116
+ | 平时怎么和会话说 | [怎么和会话说话](docs/v2/guide/00-how-to-talk.md) |
117
+ | 理解固定流程和 run 配置 | [流程与配置](docs/v2/guide/02-workflow-guide.md) |
118
+ | 批准、拒绝、证据 | [决定与证据](docs/v2/guide/07-approval-guide.md) |
119
+ | 换会话、换人接手 | [跨会话接续](docs/v2/guide/11-session-handoff.md) |
120
+ | 参与贡献 | [贡献指南](CONTRIBUTING.md) · [MIT 许可证](LICENSE) |
@@ -1,6 +1,6 @@
1
1
  # BuildBeat capability matrix / 能力矩阵
2
2
 
3
- Current release: 4.2.0. One product path: Skill → local runtime →
3
+ Current release: 4.2.2. One product path: Skill → local runtime →
4
4
  project records. The Claude Code plugin distributes the Skill and references;
5
5
  install the runtime separately. Files remain readable without the runtime, but
6
6
  manual work has no automatic loop, isolation, budget or approval enforcement;
package/docs/RELEASING.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This runbook governs BuildBeat's public npm distribution. The canonical package is `@haiyangbg/buildbeat` in `HaiYangBG1/BuildBeat`; the only executable is `buildbeat`. No other package name or executable alias receives publications.
4
4
 
5
- Release evidence at source package version `@haiyangbg/buildbeat@4.2.0`; latest independently verified BuildBeat npm distribution `@haiyangbg/buildbeat@4.1.0` (dist-tag `latest`; `next` stays on `4.0.0` until the owner moves it with npm 2FA), anchored by annotated tag `v4.1.0` at commit `9259ac9`, workflow run [37590027989](https://github.com/HaiYangBG1/BuildBeat/actions/runs/37590027989), and archived in [`V4.1.0-RELEASE-EVIDENCE-2026-10-07.md`](releases/V4.1.0-RELEASE-EVIDENCE-2026-10-07.md). The 4.0.0 chain (`latest` from 2026-10-05 until 4.1.0 took over on 2026-10-07; the owner moved `next` from `3.3.1` to `4.0.0` with npm 2FA on 2026-10-05) stays archived in [`V4.0.0-RELEASE-EVIDENCE-2026-10-05.md`](releases/V4.0.0-RELEASE-EVIDENCE-2026-10-05.md). The 3.3.1 chain (`latest` from 2026-10-02 until 4.0.0 took over on 2026-10-05; the owner moved `next` from `3.3.0` to `3.3.1` with npm 2FA on 2026-10-02) stays archived in [`V3.3.1-RELEASE-EVIDENCE-2026-10-02.md`](releases/V3.3.1-RELEASE-EVIDENCE-2026-10-02.md). The 3.3.0 chain (`latest` on 2026-10-02 until 3.3.1 took over the same day; the owner moved `next` from `3.2.1` to `3.3.0` with npm 2FA that day) stays archived in [`V3.3.0-RELEASE-EVIDENCE-2026-10-02.md`](releases/V3.3.0-RELEASE-EVIDENCE-2026-10-02.md). The 3.2.1 chain (`latest` from 2026-09-26 until 3.3.0 took over on 2026-10-02; the owner moved `next` from `3.0.1` to `3.2.1` with npm 2FA on 2026-09-26) stays archived in [`V3.2.1-RELEASE-EVIDENCE-2026-09-26.md`](releases/V3.2.1-RELEASE-EVIDENCE-2026-09-26.md). The 3.2.0 chain (`latest` on 2026-09-26 until 3.2.1 took over the same day) stays archived in [`V3.2.0-RELEASE-EVIDENCE-2026-09-26.md`](releases/V3.2.0-RELEASE-EVIDENCE-2026-09-26.md). The 3.1.0 chain (`latest` on 2026-09-26 until 3.2.0 took over the same day) stays archived in [`V3.1.0-RELEASE-EVIDENCE-2026-09-26.md`](releases/V3.1.0-RELEASE-EVIDENCE-2026-09-26.md). The 3.0.1 chain (`latest` from 2026-09-09 until 3.1.0 took over on 2026-09-26) stays archived in [`V3.0.1-RELEASE-EVIDENCE-2026-09-09.md`](releases/V3.0.1-RELEASE-EVIDENCE-2026-09-09.md). The 3.0.0 chain (`latest` from 2026-09-09 until 3.0.1 took over the same day; the first version without the removed generation) stays archived in [`V3.0.0-RELEASE-EVIDENCE-2026-09-09.md`](releases/V3.0.0-RELEASE-EVIDENCE-2026-09-09.md). The 2.0.2 chain (`latest` from 2026-09-09 until 3.0.0 took over the same day; the last version carrying the removed generation) stays archived in [`V2.0.2-RELEASE-EVIDENCE-2026-09-09.md`](releases/V2.0.2-RELEASE-EVIDENCE-2026-09-09.md). The 2.0.1 chain (`latest` from 2026-09-06 until 2.0.2 took over on 2026-09-09) stays archived in [`V2.0.1-RELEASE-EVIDENCE-2026-09-06.md`](releases/V2.0.1-RELEASE-EVIDENCE-2026-09-06.md). The 2.0.0 chain (`latest` from 2026-09-05 until 2.0.1 took over on 2026-09-06) stays archived in [`V2.0.0-RELEASE-EVIDENCE-2026-09-05.md`](releases/V2.0.0-RELEASE-EVIDENCE-2026-09-05.md). The beta.5 chain stays archived in [`V2.0.0-BETA.5-RELEASE-EVIDENCE-2026-09-05.md`](releases/V2.0.0-BETA.5-RELEASE-EVIDENCE-2026-09-05.md). The beta.4 chain stays archived in [`V2.0.0-BETA.4-RELEASE-EVIDENCE-2026-09-03.md`](releases/V2.0.0-BETA.4-RELEASE-EVIDENCE-2026-09-03.md); the beta.3 chain in [`V2.0.0-BETA.3-RELEASE-EVIDENCE-2026-09-01.md`](releases/V2.0.0-BETA.3-RELEASE-EVIDENCE-2026-09-01.md). The beta.2 chain stays archived in [`V2.0.0-BETA.2-RELEASE-EVIDENCE-2026-08-28.md`](releases/V2.0.0-BETA.2-RELEASE-EVIDENCE-2026-08-28.md); the beta.1 chain stays archived in [`V2.0.0-BETA.1-RELEASE-EVIDENCE-2026-08-28.md`](releases/V2.0.0-BETA.1-RELEASE-EVIDENCE-2026-08-28.md). Earlier distributions and the retired legacy package name are archived in the dated evidence files under `docs/releases/`.
5
+ Release evidence at source package version `@haiyangbg/buildbeat@4.2.2`; latest independently verified BuildBeat npm distribution `@haiyangbg/buildbeat@4.2.1` (dist-tag `latest`; `next` stays on `4.2.0` until the owner moves it with npm 2FA), anchored by annotated tag `v4.2.1` at commit `8d2625b`, workflow run [37626818833](https://github.com/HaiYangBG1/BuildBeat/actions/runs/37626818833), and archived in [`V4.2.1-RELEASE-EVIDENCE-2026-10-07.md`](releases/V4.2.1-RELEASE-EVIDENCE-2026-10-07.md). The 4.2.0 chain (`latest` on 2026-10-07 until 4.2.1 took over the same day; the owner moved `next` from `4.0.0` to `4.2.0` with npm 2FA that day) stays archived in [`V4.2.0-RELEASE-EVIDENCE-2026-10-07.md`](releases/V4.2.0-RELEASE-EVIDENCE-2026-10-07.md). The 4.1.0 chain (`latest` on 2026-10-07 until 4.2.0 took over the same day) stays archived in [`V4.1.0-RELEASE-EVIDENCE-2026-10-07.md`](releases/V4.1.0-RELEASE-EVIDENCE-2026-10-07.md). The 4.0.0 chain (`latest` from 2026-10-05 until 4.1.0 took over on 2026-10-07; the owner moved `next` from `3.3.1` to `4.0.0` with npm 2FA on 2026-10-05) stays archived in [`V4.0.0-RELEASE-EVIDENCE-2026-10-05.md`](releases/V4.0.0-RELEASE-EVIDENCE-2026-10-05.md). The 3.3.1 chain (`latest` from 2026-10-02 until 4.0.0 took over on 2026-10-05; the owner moved `next` from `3.3.0` to `3.3.1` with npm 2FA on 2026-10-02) stays archived in [`V3.3.1-RELEASE-EVIDENCE-2026-10-02.md`](releases/V3.3.1-RELEASE-EVIDENCE-2026-10-02.md). The 3.3.0 chain (`latest` on 2026-10-02 until 3.3.1 took over the same day; the owner moved `next` from `3.2.1` to `3.3.0` with npm 2FA that day) stays archived in [`V3.3.0-RELEASE-EVIDENCE-2026-10-02.md`](releases/V3.3.0-RELEASE-EVIDENCE-2026-10-02.md). The 3.2.1 chain (`latest` from 2026-09-26 until 3.3.0 took over on 2026-10-02; the owner moved `next` from `3.0.1` to `3.2.1` with npm 2FA on 2026-09-26) stays archived in [`V3.2.1-RELEASE-EVIDENCE-2026-09-26.md`](releases/V3.2.1-RELEASE-EVIDENCE-2026-09-26.md). The 3.2.0 chain (`latest` on 2026-09-26 until 3.2.1 took over the same day) stays archived in [`V3.2.0-RELEASE-EVIDENCE-2026-09-26.md`](releases/V3.2.0-RELEASE-EVIDENCE-2026-09-26.md). The 3.1.0 chain (`latest` on 2026-09-26 until 3.2.0 took over the same day) stays archived in [`V3.1.0-RELEASE-EVIDENCE-2026-09-26.md`](releases/V3.1.0-RELEASE-EVIDENCE-2026-09-26.md). The 3.0.1 chain (`latest` from 2026-09-09 until 3.1.0 took over on 2026-09-26) stays archived in [`V3.0.1-RELEASE-EVIDENCE-2026-09-09.md`](releases/V3.0.1-RELEASE-EVIDENCE-2026-09-09.md). The 3.0.0 chain (`latest` from 2026-09-09 until 3.0.1 took over the same day; the first version without the removed generation) stays archived in [`V3.0.0-RELEASE-EVIDENCE-2026-09-09.md`](releases/V3.0.0-RELEASE-EVIDENCE-2026-09-09.md). The 2.0.2 chain (`latest` from 2026-09-09 until 3.0.0 took over the same day; the last version carrying the removed generation) stays archived in [`V2.0.2-RELEASE-EVIDENCE-2026-09-09.md`](releases/V2.0.2-RELEASE-EVIDENCE-2026-09-09.md). The 2.0.1 chain (`latest` from 2026-09-06 until 2.0.2 took over on 2026-09-09) stays archived in [`V2.0.1-RELEASE-EVIDENCE-2026-09-06.md`](releases/V2.0.1-RELEASE-EVIDENCE-2026-09-06.md). The 2.0.0 chain (`latest` from 2026-09-05 until 2.0.1 took over on 2026-09-06) stays archived in [`V2.0.0-RELEASE-EVIDENCE-2026-09-05.md`](releases/V2.0.0-RELEASE-EVIDENCE-2026-09-05.md). The beta.5 chain stays archived in [`V2.0.0-BETA.5-RELEASE-EVIDENCE-2026-09-05.md`](releases/V2.0.0-BETA.5-RELEASE-EVIDENCE-2026-09-05.md). The beta.4 chain stays archived in [`V2.0.0-BETA.4-RELEASE-EVIDENCE-2026-09-03.md`](releases/V2.0.0-BETA.4-RELEASE-EVIDENCE-2026-09-03.md); the beta.3 chain in [`V2.0.0-BETA.3-RELEASE-EVIDENCE-2026-09-01.md`](releases/V2.0.0-BETA.3-RELEASE-EVIDENCE-2026-09-01.md). The beta.2 chain stays archived in [`V2.0.0-BETA.2-RELEASE-EVIDENCE-2026-08-28.md`](releases/V2.0.0-BETA.2-RELEASE-EVIDENCE-2026-08-28.md); the beta.1 chain stays archived in [`V2.0.0-BETA.1-RELEASE-EVIDENCE-2026-08-28.md`](releases/V2.0.0-BETA.1-RELEASE-EVIDENCE-2026-08-28.md). Earlier distributions and the retired legacy package name are archived in the dated evidence files under `docs/releases/`.
6
6
 
7
7
  ## Channels and branches
8
8
 
@@ -10,4 +10,6 @@ Review input includes adjudication anchors and an available lastReviewed increme
10
10
 
11
11
  Use exit 75 for missing tools, unavailable backends or an unusable execution environment. Do not change business code to hide an environment failure. Do not print credentials. The runner reads candidate, exit status and evidence itself.
12
12
 
13
+ An invalid envelope is `invalid-output`, an environment failure, never a reason to change business code. A review step that exits 0 without writing its report to `$BUILDBEAT_OUTPUT` (printing it to stdout is not enough) is `invalid-output` too and stops for a person; a clean review hands in `{"findings": []}`.
14
+
13
15
  `BUILDBEAT_INPUT.workArtifact` carries the selected repository-relative `ref` and accepted `digest`. Read that artifact; use work.md first and legacy intent/plan only when work.md is absent.
@@ -10,6 +10,6 @@ reviewer 输入包含历史裁决 anchor 和可用的 lastReviewed 增量范围
10
10
 
11
11
  环境缺工具、后端不可用、沙箱禁止必要操作等用 exit 75;不要通过修改业务代码掩盖环境问题。日志不得主动输出凭据。实际候选、退出码和证据都由内核回读,自述不能替代事实。
12
12
 
13
- 无效信封记为 `invalid-output`,属于环境故障,不能据此要求修改业务代码。
13
+ 无效信封记为 `invalid-output`,属于环境故障,不能据此要求修改业务代码。审查步骤退出 0 却没有把报告写进 `$BUILDBEAT_OUTPUT`(只打印在 stdout 不算)同样记为 `invalid-output` 并停人;没有问题也要交 `{"findings": []}`。
14
14
 
15
15
  `BUILDBEAT_INPUT.workArtifact` carries the selected repository-relative `ref` and accepted `digest`. Read that artifact; use work.md first and legacy intent/plan only when work.md is absent.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haiyangbg/buildbeat",
3
- "version": "4.2.0",
3
+ "version": "4.2.2",
4
4
  "description": "BuildBeat: Git-based AI delivery across models, tools, sessions, and people, with context in project files, build-test-review-fix loops, and human decisions.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1073,6 +1073,13 @@ function recordStepResult(
1073
1073
  envelopeRaw = readFileSync(outputPath, "utf8");
1074
1074
  }
1075
1075
  const { envelope, error: envelopeError } = parseEnvelope(envelopeRaw);
1076
+ // A review that exits cleanly without a report reviewed nothing the
1077
+ // kernel can check: no evidence, no findings. Real incident: a reviewer
1078
+ // printed 3 P1, 6 P2 and 4 P3 findings to stdout only; the step counted
1079
+ // as passed and the gap surfaced only at the merge approval.
1080
+ const reportMissing =
1081
+ isReviewStep(step, stepDef) &&
1082
+ (envelopeRaw === undefined || envelopeRaw === null);
1076
1083
 
1077
1084
  let stepStatus;
1078
1085
  if (exec.spawnError) {
@@ -1087,11 +1094,20 @@ function recordStepResult(
1087
1094
  // The command passed but a requirement the runner checks did not
1088
1095
  // (no screenshots): a candidate failure, routed like any other.
1089
1096
  stepStatus = "failed";
1090
- } else if (envelopeError) {
1097
+ } else if (envelopeError || reportMissing) {
1091
1098
  stepStatus = "invalid-output";
1092
1099
  } else {
1093
1100
  stepStatus = "succeeded";
1094
1101
  }
1102
+ let outputProblem = null;
1103
+ if (stepStatus === "invalid-output" && !envelopeError) {
1104
+ const stdoutBytes = Buffer.byteLength(String(exec.stdout ?? "").trim());
1105
+ outputProblem =
1106
+ "the reviewer exited 0 without writing a report to $BUILDBEAT_OUTPUT (stdout is not read)" +
1107
+ (stdoutBytes > 0
1108
+ ? `; stdout has ${stdoutBytes} bytes; write the JSON report to $BUILDBEAT_OUTPUT`
1109
+ : "");
1110
+ }
1095
1111
  // Infrastructure failure vs candidate failure. A timeout, a crash,
1096
1112
  // garbage output or the worker's own "environment unavailable" signal
1097
1113
  // (exit 75, EX_TEMPFAIL) says nothing about the candidate: no failure
@@ -1116,6 +1132,7 @@ function recordStepResult(
1116
1132
  status: stepStatus,
1117
1133
  exitCode: exec.exitCode,
1118
1134
  ...(exec.requirementFailure ? { reason: exec.requirementFailure } : {}),
1135
+ ...(outputProblem ? { reason: outputProblem } : {}),
1119
1136
  ...(exec.screenshotsRejected
1120
1137
  ? { screenshotsRejected: exec.screenshotsRejected }
1121
1138
  : {}),
@@ -1138,7 +1155,7 @@ function recordStepResult(
1138
1155
  stepStatus === "failed"
1139
1156
  ? "exit 75 (worker reports its environment unavailable)"
1140
1157
  : stepStatus === "invalid-output"
1141
- ? "output is not a worker envelope"
1158
+ ? (outputProblem ?? "output is not a worker envelope")
1142
1159
  : stepStatus;
1143
1160
  context.waitHuman(
1144
1161
  `resume-${step}`,
@@ -12,12 +12,29 @@ import { join } from "node:path";
12
12
  import { EventLedger } from "../storage/event-ledger.js";
13
13
  import { readFindingsAccount } from "./findings.js";
14
14
 
15
+ // How each human wait ended. Counting waits alone hid where the time went:
16
+ // one work showed 32 minutes of decided waits while 11 hours had passed on
17
+ // a request nobody answered before a new run superseded it.
18
+ export const WAIT_KINDS = ["decided", "superseded", "stopped", "open"];
19
+
20
+ function emptyWaits() {
21
+ return Object.fromEntries(WAIT_KINDS.map((kind) => [kind, { count: 0, ms: 0 }]));
22
+ }
23
+
24
+ function addWait(bucket, ms) {
25
+ bucket.count += 1;
26
+ if (Number.isFinite(ms) && ms > 0) {
27
+ bucket.ms += ms;
28
+ }
29
+ }
30
+
15
31
  function emptyCost() {
16
32
  return {
17
33
  runs: 0,
18
34
  reviewRounds: 0,
19
35
  findings: 0,
20
36
  humanWaits: 0,
37
+ waits: emptyWaits(),
21
38
  infraFailures: 0,
22
39
  workerMs: 0,
23
40
  firstAt: null,
@@ -25,11 +42,25 @@ function emptyCost() {
25
42
  };
26
43
  }
27
44
 
28
- // Cost facts of one ledger: review rounds, human waits, infra failures and
29
- // worker wall time (STEP_STARTED → STEP_FINISHED per attempt).
30
- export function ledgerCost(ledger) {
31
- const cost = { reviewRounds: 0, humanWaits: 0, infraFailures: 0, workerMs: 0 };
45
+ // Cost facts of one ledger: review rounds, human waits and how each ended,
46
+ // infra failures and worker wall time (STEP_STARTED → STEP_FINISHED per
47
+ // attempt). A wait runs from HUMAN_REQUESTED to the next DECISION_RECORDED
48
+ // (decided) or to a RUN_TERMINAL without a decision (superseded or
49
+ // stopped); a run still waiting counts up to `now` (open). A request
50
+ // repeated before any decision (a resumed run asks again for the same
51
+ // decision) continues the same wait from the first request: counting it
52
+ // again would count the overlapping time twice.
53
+ export function ledgerCost(ledger, { now = Date.now() } = {}) {
54
+ const cost = {
55
+ reviewRounds: 0,
56
+ humanWaits: 0,
57
+ waits: emptyWaits(),
58
+ infraFailures: 0,
59
+ workerMs: 0,
60
+ };
32
61
  const open = new Map();
62
+ let waitingSince = null;
63
+ let terminal = false;
33
64
  for (const event of ledger.events) {
34
65
  if (event.type === "STEP_STARTED") {
35
66
  open.set(`${event.data.step}#${event.data.attempt}`, Date.parse(event.ts));
@@ -51,12 +82,38 @@ export function ledgerCost(ledger) {
51
82
  }
52
83
  } else if (event.type === "HUMAN_REQUESTED") {
53
84
  cost.humanWaits += 1;
85
+ waitingSince ??= Date.parse(event.ts);
86
+ } else if (event.type === "DECISION_RECORDED") {
87
+ if (waitingSince !== null) {
88
+ addWait(cost.waits.decided, Date.parse(event.ts) - waitingSince);
89
+ waitingSince = null;
90
+ }
91
+ } else if (event.type === "RUN_TERMINAL") {
92
+ terminal = true;
93
+ if (waitingSince !== null) {
94
+ const kind = event.data?.status === "SUPERSEDED" ? "superseded" : "stopped";
95
+ addWait(cost.waits[kind], Date.parse(event.ts) - waitingSince);
96
+ waitingSince = null;
97
+ }
54
98
  }
55
99
  }
100
+ if (waitingSince !== null && !terminal) {
101
+ addWait(cost.waits.open, now - waitingSince);
102
+ }
56
103
  return cost;
57
104
  }
58
105
 
59
- export function computeWorkCost(repoRoot, workId, { excludeRun = null } = {}) {
106
+ function addWaits(total, waits) {
107
+ for (const kind of WAIT_KINDS) {
108
+ const row = waits?.[kind];
109
+ if (row && Number.isFinite(row.count) && Number.isFinite(row.ms)) {
110
+ total[kind].count += row.count;
111
+ total[kind].ms += row.ms;
112
+ }
113
+ }
114
+ }
115
+
116
+ export function computeWorkCost(repoRoot, workId, { excludeRun = null, now = Date.now() } = {}) {
60
117
  const cost = emptyCost();
61
118
  const seen = new Set();
62
119
  const runsDir = join(repoRoot, ".buildbeat", "runtime", "runs");
@@ -75,10 +132,11 @@ export function computeWorkCost(repoRoot, workId, { excludeRun = null } = {}) {
75
132
  if (state.run.id === excludeRun) {
76
133
  continue;
77
134
  }
78
- const row = ledgerCost(ledger);
135
+ const row = ledgerCost(ledger, { now });
79
136
  cost.runs += 1;
80
137
  cost.reviewRounds += row.reviewRounds;
81
138
  cost.humanWaits += row.humanWaits;
139
+ addWaits(cost.waits, row.waits);
82
140
  cost.infraFailures += row.infraFailures;
83
141
  cost.workerMs += row.workerMs;
84
142
  track(cost, ledger.events[0]?.ts, ledger.events[ledger.events.length - 1]?.ts);
@@ -105,6 +163,8 @@ export function computeWorkCost(repoRoot, workId, { excludeRun = null } = {}) {
105
163
  // only; the cost block is preferred when present.
106
164
  cost.reviewRounds += record.cost?.reviewRounds ?? record.attempts?.review ?? 0;
107
165
  cost.humanWaits += record.cost?.humanWaits ?? record.decisions?.length ?? 0;
166
+ // Records written before wait kinds existed add no durations.
167
+ addWaits(cost.waits, record.cost?.waits);
108
168
  cost.infraFailures += record.cost?.infraFailures ?? 0;
109
169
  cost.workerMs += record.cost?.workerMs ?? 0;
110
170
  track(cost, record.startedAt, record.finishedAt);
@@ -137,10 +197,13 @@ export function formatWorkerMs(ms) {
137
197
  }
138
198
 
139
199
  export function renderWorkCost(cost) {
200
+ const waits = WAIT_KINDS.filter((kind) => cost.waits?.[kind]?.count > 0).map(
201
+ (kind) => `${kind} ${formatWorkerMs(cost.waits[kind].ms)}`,
202
+ );
140
203
  return [
141
204
  `review rounds ${cost.reviewRounds}`,
142
205
  `findings ${cost.findings}`,
143
- `human waits ${cost.humanWaits}`,
206
+ `human waits ${cost.humanWaits}${waits.length > 0 ? ` (${waits.join(" · ")})` : ""}`,
144
207
  ...(cost.infraFailures > 0 ? [`infra failures ${cost.infraFailures}`] : []),
145
208
  `worker ${formatWorkerMs(cost.workerMs)}`,
146
209
  ].join(" · ");