@haiyangbg/buildbeat 4.2.0 → 4.2.1

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,16 @@
2
2
 
3
3
  > 本项目吃自己的狗粮(红线④:必更 CHANGELOG)。格式循 Keep a Changelog,倒序。
4
4
 
5
+ ## v4.2.1 — 2026-10-07(补丁:审查没交报告当场停;成本按去向分开等待)
6
+
7
+ - 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.
8
+ - 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.
9
+ - 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.
10
+
5
11
  ## v4.2.0 — 2026-10-07(次版本:合并决定点能接修复;合并批准绑定候选)
6
12
 
13
+ > **发布状态**:`@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。
14
+
7
15
  - 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
16
  - 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
17
  - 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
@@ -2,11 +2,13 @@
2
2
 
3
3
  **Switch sessions. Keep building.**
4
4
 
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.
5
+ BuildBeat is for humans and AI sessions: the work description and every record 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.
6
6
 
7
7
  ## Three promises
8
8
 
9
- Switch sessions without losing progress; let the loop implement, verify, review and repair; inspect the candidate and evidence before deciding.
9
+ - **Walk away.** Each work gets one `work.md`; once accepted, `buildbeat run` drives it. Close the session, switch AI tools or hand over to someone else, and continue from the project files and the run ledger. In a main repository with several code repositories, `buildbeat status --repo . --all-repos` shows what is pending everywhere.
10
+ - **Come back to facts.** Verification runs the project's own commands, and the runtime reads results back from Git and the ledger instead of trusting what an AI says; review is independent and read-only. Missing tools, an unavailable backend or a review that hands in no report stop as infrastructure failures for a person, instead of being "fixed" as code.
11
+ - **Decide with confidence.** A merge approval is bound to the candidate you reviewed (`--candidate`). When you spot a small problem at the merge decision, commit a fix and hand it over (`run --adopt`) or send it back to the fixer in one line (`decide --action fix`), without starting a new run. Merge, push, release and deployment stay with people.
10
12
 
11
13
  ## Start
12
14
 
@@ -26,13 +28,13 @@ buildbeat status --repo . --work WORK-X
26
28
 
27
29
  ## Records and handoffs
28
30
 
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.
31
+ End-to-end work packages contain work.md, configuration, decisions, review findings and terminal records under delivery/work/. Local in-flight events and logs live in .buildbeat/runtime/; candidates live in .buildbeat/worktrees/. The cost line in `status` shows review rounds, findings and where human waits went (decided, superseded, stopped, still open). 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.
30
32
 
31
- [Handoff](docs/v2/guide/11-session-handoff.en.md) · [Recovery](docs/v2/guide/10-recovery.en.md)
33
+ [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
34
 
33
35
  ## Boundaries
34
36
 
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.
37
+ 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
38
 
37
39
  [Capabilities](docs/CAPABILITY-MATRIX.md) · [Security](docs/v2/guide/09-security-boundaries.en.md) · [Migration](docs/MIGRATION.md)
38
40
 
package/README.md CHANGED
@@ -2,11 +2,13 @@
2
2
 
3
3
  **会话随时换,项目接着干。**
4
4
 
5
- BuildBeat 面向人和 AI 会话,把工作上下文留在项目文件中,自动推进可恢复的交付循环,带证据交给人决定。
5
+ BuildBeat 面向人和 AI 会话:工作说明和全部记录留在项目文件与 Git 里,实现、验证、审查和修复在会话之外自动推进、随时可恢复,候选带着证据停在你的合并决定前。
6
6
 
7
7
  ## 三个承诺
8
8
 
9
- 换会话能接着干;实现、验证、审查和修复自动推进;候选和证据就绪后再做决定。
9
+ - **走得开**:一项工作写一份 `work.md`,接受后由 `buildbeat run` 推进。关掉会话、换 AI 工具、换人,都能从项目文件和运行台账接着干;主仓带多个代码仓时,`buildbeat status --repo . --all-repos` 一条命令看全所有仓的待办。
10
+ - **回来能信**:验证跑项目自己的命令,结果由运行时从 Git 和台账回读,不听 AI 自述;审查独立、只读。缺工具、后端不可用、审查没交报告,都按环境故障停下等人,不会把环境问题当成代码问题去修。
11
+ - **敢拍板**:合并批准绑定你看过的那一版候选(`--candidate`)。在合并决定点发现小问题,可以自己修好交回(`run --adopt`),或一句话退回给修复者(`decide --action fix`),不必重开一轮。合并、推送、发布、部署始终由人决定。
10
12
 
11
13
  ## 开始使用
12
14
 
@@ -26,13 +28,13 @@ buildbeat status --repo . --work WORK-X
26
28
 
27
29
  ## 记录与接续
28
30
 
29
- 端到端工作包在 delivery/work/ 保存 work.md、配置、决定与终态记录;本机在途事件和日志在 .buildbeat/runtime/,候选在 .buildbeat/worktrees/。换会话前补齐未落盘事实。Git clone 不迁移活动进程和现场。
31
+ 端到端工作包在 delivery/work/ 保存 work.md、配置、决定、审查问题与终态记录;本机在途事件和日志在 .buildbeat/runtime/,候选在 .buildbeat/worktrees/。`status` 的成本行列出审查轮数、问题数,以及人工等待各自的去向(已决定、被替代、被停掉、仍在等)。换会话前补齐未落盘事实;Git clone 不迁移活动进程和现场。没装运行时的人和工具也能读状态、写工作说明。
30
32
 
31
- [Handoff](docs/v2/guide/11-session-handoff.md) · [Recovery](docs/v2/guide/10-recovery.md)
33
+ [Handoff](docs/v2/guide/11-session-handoff.md) · [Recovery](docs/v2/guide/10-recovery.md) · [没有运行时](docs/v2/guide/12-without-runtime.md)
32
34
 
33
35
  ## 能力边界
34
36
 
35
- BuildBeat 不提供多人账号、角色/权限系统,不采集或上传项目使用数据,没有遥测采集。模型和鉴权由配置的 AI 工具提供。合并、推送、部署、发布分别由获授权的外部操作完成。生产巡检、上线专用流程和任意工作流/规则语言已移出产品。
37
+ BuildBeat 不提供多人账号、角色/权限系统,不采集或上传项目使用数据,没有遥测采集。模型和鉴权由配置的 AI 工具提供。合并、推送、部署、发布分别由获授权的外部操作完成;上线后可用 `buildbeat release` 记录项目自己的回读,回读通过才关窗。生产巡检和任意工作流/规则语言不在产品范围内。
36
38
 
37
39
  [Capabilities](docs/CAPABILITY-MATRIX.md) · [Security](docs/v2/guide/09-security-boundaries.md) · [Migration](docs/MIGRATION.md)
38
40
 
@@ -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.1. 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.1`; latest independently verified BuildBeat npm distribution `@haiyangbg/buildbeat@4.2.0` (dist-tags `latest` and `next`; the owner moved `next` from `4.0.0` to `4.2.0` with npm 2FA on 2026-10-07), anchored by annotated tag `v4.2.0` at commit `cc52ad3`, workflow run [37597691057](https://github.com/HaiYangBG1/BuildBeat/actions/runs/37597691057), and 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.1",
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(" · ");