@haiyangbg/buildbeat 3.3.0 → 3.3.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 +7 -0
- package/docs/RELEASING.md +4 -1
- package/docs/v2/guide/02-workflow-guide.en.md +1 -1
- package/docs/v2/guide/02-workflow-guide.md +1 -1
- package/docs/v2/guide/07-approval-guide.md +1 -1
- package/docs/v2/skill/04-rhythm-and-rituals.md +1 -1
- package/lessons.md +7 -1
- package/package.json +1 -1
- package/src/v2/cli/run.js +3 -3
- package/src/v2/runtime/findings.js +70 -0
- package/src/v2/runtime/orchestrator.js +18 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
> 本项目吃自己的狗粮(红线④:必更 CHANGELOG)。格式循 Keep a Changelog,倒序。
|
|
4
4
|
|
|
5
|
+
## v3.3.1 — 2026-10-02(补丁:review 收敛判断认得出换了说法的同一问题;lessons 与发布 runbook 补齐)
|
|
6
|
+
|
|
7
|
+
- **review 收敛判断认得出换了说法的同一问题**:3.3.0 判断「修过的 finding 又出现」只认相同指纹(严重度 + 描述原文),而 reviewer 每轮会换行号、改措辞——回放 4 个仓 161 轮 review,按指纹一次都没对上,同一个问题却在一个 Run 里被连审三轮。新增 `sameIssue`:同指纹,或引用同一组文件且描述相近(字符二元组 Dice ≥ 0.5),或描述高度相近(≥ 0.6);严重度不参与(P1 升 P0 仍算同一问题),同一组文件下描述只差行号也算;描述去掉文件与数字后不足 16 字时只认这两种精确情况。路径可以用反引号包起来。只用于收敛判断,finding 指纹、裁决台账与 dismiss 抑制不变。停人理由里换了说法的一条写成「本轮指纹 restates round N 原指纹」。阈值依据:回放中真实改写全部命中,无关 finding 最高 0.57、同文件无关 finding 最高 0.33。
|
|
8
|
+
- **文档补齐**:`lessons.md` 教训 14 的解药改为现行机制(原文仍写每 Run 2 轮封顶、分诊后才派 fixer),新增教训 23(中途审批点被驾驶会话自批、同一问题换说法认不出);发布 runbook 要求候选用与发布 workflow 相同的 Node / npm 打包(否则 gzip 层不同、integrity 对不上,改比解压后的 tar),发布后检查 `which -a buildbeat` 列出的每一份全局安装;3.3.0 发布证据更正本机只更新了一份全局安装的事实。
|
|
9
|
+
|
|
5
10
|
## v3.3.0 — 2026-10-02(review 少停人:收敛止损、单一轮数上限、模板关分诊、intent+plan 一次接受)
|
|
6
11
|
|
|
12
|
+
> **发布状态**:`@haiyangbg/buildbeat@3.3.0` 已于 2026-10-02 从 `main`(PR #59、#62、#60、#61、#45 内容,release PR #63,merge commit `1ce1a7e`,tag `v3.3.0`)经 OIDC Trusted Publishing 发布到 dist-tag **`latest`**(run 36973733935,publish 与 verify 双 job 一次 success;所有者授权「发 3.3.0」)。独立回读(直连 npmjs.org):`latest` = 3.3.0、发布包解压后的 tar 与发布前本地候选逐字节相同(只有 gzip 压缩层不同:本地 Node 22、CI Node 24,故 integrity 不同)、SLSA v1 provenance、隔离安装 `--version` = 3.3.0、裸调用零写入、已发布包的 `doctor` 显示 review 上限来源 `reviewRoundsPerWork`、`npm audit signatures` 通过;GitHub Release v3.3.0 标 Latest,证据见 [`docs/releases/V3.3.0-RELEASE-EVIDENCE-2026-10-02.md`](docs/releases/V3.3.0-RELEASE-EVIDENCE-2026-10-02.md)。同日所有者本人经 npm 两步验证把 dist-tag `next` 从 3.2.1 挪到 3.3.0;直连 npmjs.org 回读 `next` = `latest` = 3.3.0。
|
|
13
|
+
|
|
7
14
|
- **review 按收敛止损,不再按 2 轮封顶**:review 发现阻断问题、派 fixer 之前,内核把本轮 P0/P1 与本 Run 之前各轮比较。修过的 finding 又出现(同指纹,已 dismiss 的不算),或本轮阻断数多于上一轮,就停 `enter-fix`(新 kind `review-not-converging`,理由列出又出现的指纹或前后数量,请求不带 grants,批准不动预算);阻断 finding 都是新的、数量不多于上一轮时自动继续。官方预设不再自带每 Run 2 轮的 `budgets.maxAttempts.review`,轮数上限改由下一条的 `reviewRoundsPerWork` 统一负责;run 配置里显式写的值照旧优先。开了 `reviewTriage: required` 时仍停分诊,不收敛理由并入同一请求;`nextReply` 对新 kind 给出 `findings list` / `findings adjudicate` 命令。原因:每 Run 2 轮封顶让正常修两三轮的 Run 反复停下批准扩额;本仓最近 10 个 Run 里中途 9 次批准都由驾驶会话自己批,人只出现在合并决定,审批点没起到人把关的作用。
|
|
8
15
|
- **review 轮数只剩一个上限**:`budgets.reviewRoundsPerWork` 不写时默认 6(此前不写即没有 Work 级上限);review 步没有显式 `maxAttempts` 时,每 Run 上限取同一个值,不会先于 Work 上限触发,官方预设不再自带 `maxAttempts.review`。`doctor` 显示 review 上限来源为 `reviewRoundsPerWork` 并标出默认值;run 配置样板删去 `maxAttempts.review`。显式写的 `maxAttempts.review` 照旧生效。原因:每 Run 与每 Work 两套计数让同一轮 review 要按两层上限解释和放行,止损线本来就按 Work 定。
|
|
9
16
|
- **模板默认不再开发现分诊门**:`templates/v2/run-config.example.yaml`、示例项目、`SKILL.md` 与快速开始里的 run 配置样板从 `reviewTriage: required` 改为 `reviewTriage: off`,P0/P1 finding 直接派 fixer;高风险项目仍可改回 `required`。内核默认值本来就是不分诊,未改动;已有项目的 run 配置不受影响。原因:本仓最近 10 个 Run 中途 9 次停人里 7 次是分诊门的 `enter-fix`,且全部由驾驶会话自己批准,没有起到人把关的作用。
|
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@3.3.
|
|
5
|
+
Release evidence at source package version `@haiyangbg/buildbeat@3.3.1`; latest independently verified BuildBeat npm distribution `@haiyangbg/buildbeat@3.3.0` (dist-tags `latest` and `next`; the owner moved `next` from `3.2.1` to `3.3.0` with npm 2FA on 2026-10-02), anchored by annotated tag `v3.3.0` at commit `1ce1a7e`, workflow run [36973733935](https://github.com/HaiYangBG1/BuildBeat/actions/runs/36973733935), and 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
|
|
|
@@ -44,6 +44,8 @@ gitleaks git --no-banner --redact --no-color
|
|
|
44
44
|
git diff --check
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
Record the candidate's integrity from a pack made with the same toolchain as `publish.yml` (Node 24, npm 11.19.0), for example `npx --yes npm@11.19.0 pack --json` under Node 24. A pack from another Node major compresses differently, so its `integrity` will not equal the registry's even when the contents are identical (3.3.0: candidate packed under Node 22). When the toolchains differ, compare the SHA-256 of the decompressed tarballs (`gunzip -c <tgz> | shasum -a 256`) and the file listings instead, and say so in the evidence.
|
|
48
|
+
|
|
47
49
|
Before pushing a tag, confirm that the package name/version is absent from the official registry and that the candidate commit's `main` CI is green. An `E404` only proves point-in-time absence; it does not reserve the name.
|
|
48
50
|
|
|
49
51
|
Also read back the server-side tag rule instead of assuming that repository documentation represents current GitHub configuration:
|
|
@@ -138,5 +140,6 @@ Publishing the artifact is one surface. These are the others; each has drifted a
|
|
|
138
140
|
- [ ] Active RFC / plan documents whose channel policy the release changed get a dated "生效修订" note; history keeps its original text.
|
|
139
141
|
- [ ] GitHub repository About (description, topics, homepage) still describes the product that was just released — cannot be checked from the repository, do it by hand.
|
|
140
142
|
- [ ] GitHub Release marked Latest for a stable release, not for a pre-release.
|
|
143
|
+
- [ ] Every global install on the release operator's machine moved to the new version: `which -a buildbeat` can list more than one (a Node version manager and Homebrew keep separate global prefixes; 3.3.0 updated only one of two, and sessions kept running 3.2.1).
|
|
141
144
|
- [ ] Claude Code plugin: if `plugins/buildbeat/.claude-plugin/plugin.json` changed, its version bumped and `tests/plugin-marketplace.test.sh` updated; the plugin version is independent of the npm version.
|
|
142
145
|
- [ ] `npm run check:docs` green on the release commit (it enforces the active-document claims above).
|
|
@@ -71,7 +71,7 @@ The run config can also declare (beta.3, all from real incidents in a thirty-rou
|
|
|
71
71
|
|
|
72
72
|
Review rounds have a single cap: `budgets.reviewRoundsPerWork` (default 6), counted across every Run of this Work (see "Review rounds counted per Work" below). A review step without an explicit `maxAttempts` takes that value as its per-Run cap, so the per-Run cap never fires before the Work cap; at the cap the Run stops at `WAITING_HUMAN` with the reason stating that the budget is exhausted. `budgets.maxAttempts.review` in the run config still sets a separate per-Run cap. (The official preset used to carry the campaign charter's "at most 2 review rounds per Run", which made a Run that needed two or three ordinary fix rounds ask for an extension again and again, and two caps had to be lifted together.)
|
|
73
73
|
|
|
74
|
-
**Stop on non-convergence**: below the cap, each time review reports blocking findings, before a fixer is dispatched, the kernel compares this round's P0/P1 findings with the earlier rounds of this Run. If
|
|
74
|
+
**Stop on non-convergence**: below the cap, each time review reports blocking findings, before a fixer is dispatched, the kernel compares this round's P0/P1 findings with the earlier rounds of this Run. If the same problem comes back after it was already sent to a fixer (same fingerprint, or a restatement: the same referenced files with a close description, or a very close description; dismissed ones do not count), or this round has more blocking findings than the last one, the Run stops at `enter-fix` (kind `review-not-converging`) and the reason names the returning fingerprints (a restatement reads `<this fingerprint> restates round N <earlier fingerprint>`) or the two counts. "Close description" is used only for this convergence check; fingerprints and adjudications still match the exact text, Severity does not count (a P1 that comes back as P0 is the same problem), and the same description at moved line numbers in the same files matches at any length; a description shorter than 16 characters once files and numbers are removed matches only in those two exact ways. Approving means fix + re-verify + one more review round and leaves the budget alone; `findings adjudicate --action dismiss` first stops a finding that should not block. While every blocking finding is new and there are no more of them than last round, the loop continues without asking anyone. With `reviewTriage: required` the triage stop happens as before and carries the non-convergence reason; at the cap the stop is still `budget`, with the same reason added.
|
|
75
75
|
|
|
76
76
|
For the `resume-<step>` a Run stops at once the budget is exhausted, **a human approval grants one more attempt**: the kernel records a `BUDGET_EXTENDED` (a ledger fact, replayable), raises that step's limit by one and runs it; a rejection ends the Run. Before this, approving only made the same request come straight back (two pilot app-login Runs ended CANCELLED because of it, although their candidates were already in production).
|
|
77
77
|
|
|
@@ -71,7 +71,7 @@ run 配置还可声明(beta.3,皆来自三十轮部署战役的真实事故
|
|
|
71
71
|
|
|
72
72
|
review 轮数只有一个上限:`budgets.reviewRoundsPerWork`(默认 6),跨本 Work 所有 Run 累计(见下文"按 Work 累计的 review 轮数")。review 步没有显式 `maxAttempts` 时,它在单个 Run 内的上限就取这个值,所以不会先于 Work 上限触发;到顶时停 `WAITING_HUMAN`,理由写明预算耗尽。仍可在 run 配置里写 `budgets.maxAttempts.review` 另设每 Run 上限。(官方预设曾自带战役章程的"每 Run 2 轮封顶",正常修两三轮的 Run 也要一次次批准扩额,两层上限还要合并放行。)
|
|
73
73
|
|
|
74
|
-
**按收敛止损**:上限之内,review 每次发现阻断问题、派 fixer 之前,内核把本轮 P0/P1 与本 Run
|
|
74
|
+
**按收敛止损**:上限之内,review 每次发现阻断问题、派 fixer 之前,内核把本轮 P0/P1 与本 Run 之前各轮比较。只要同一个问题在修过之后又出现(指纹相同,或者 reviewer 换了说法:引用同一组文件且描述相近,或描述高度相近;已 dismiss 的不算),或者本轮阻断数多于上一轮,就停 `enter-fix`(kind `review-not-converging`),理由列出又出现的指纹(换了说法时写成「本轮指纹 restates round N 原指纹」)或前后两轮的数量。「描述相近」只用于这里的收敛判断,finding 指纹和裁决照旧按原文精确匹配;严重度不参与(P1 升到 P0 仍算同一问题),引用同一组文件、描述只差行号也算;描述去掉文件与数字后不足 16 个字符时只认这两种精确情况。批准 = 修复 + 重验 + 再审一轮,不动预算;也可以先 `findings adjudicate --action dismiss`,让不该阻断的 finding 不再阻断。阻断 finding 都是新的、数量也不多于上一轮时,自动继续,不问人。开了 `reviewTriage: required` 时照常停分诊,不收敛的理由并入同一条请求;到顶时仍是 `budget` 停车,同样附上不收敛的理由。
|
|
75
75
|
|
|
76
76
|
预算耗尽后停的那次 `resume-<step>`,**人批准即多给一次**:内核落一条 `BUDGET_EXTENDED`(台账事实,可重放),该步上限 +1 再跑;拒绝即终止 Run。此前批准只会让同一请求立刻回来(试点两条应用登录 Run 因此以 CANCELLED 收场,候选却已在生产)。
|
|
77
77
|
|
|
@@ -52,7 +52,7 @@ buildbeat accept --repo . --work WORK-X --artifact plan --by <名字> # 工
|
|
|
52
52
|
- 非只读步(build / verify / fix)成功的 attempt 不扣 `maxAttempts`,只读步仍消耗次数,review 按轮计费。`STEP_FINISHED.free: true` 与 `BUDGET_CONSUMED.amount: 0` 记录退款;没有新字段的旧台账保持原有回放结果。
|
|
53
53
|
- 真失败到顶仍停 `resume-<step>`,同指纹两次仍停,release 预设的 `maxAttempts: 1` 仍有效。infra 故障仍不扣次数。review 轮数只受 Work 级 `reviewRoundsPerWork`(默认 6)约束,只作兜底。
|
|
54
54
|
- review 发现阻断问题时,若下一轮会超过 Run 或 Work 上限,立即停 `enter-fix`,在花费修复、重验之前问一次。有分诊时 kind 为 `finding-triage`,无分诊时为 `budget`。批准表示「修复 + 重新验证 + 再审一轮」;拒绝结束本 Run,由人按现有证据决定是否合并。
|
|
55
|
-
- 上限之内只在 review 不收敛时停 `enter-fix
|
|
55
|
+
- 上限之内只在 review 不收敛时停 `enter-fix`:同一个问题在修过之后又出现(同指纹,或换了说法但引用同一组文件、描述相近;已 dismiss 的不算),或本轮阻断数多于上一轮。kind 为 `review-not-converging`(有分诊时仍是 `finding-triage`,理由里多一条不收敛),请求不带 grants,批准不动预算。阻断 finding 都是新的、数量不多于上一轮时自动继续。见 [Workflow 指南](02-workflow-guide.md) 的「按收敛止损」。
|
|
56
56
|
- 每一次预算停车(`enter-fix`、`resume-<step>`、Work 级 `enter-review`)都在请求上记可选 `grants`,列出下一次执行会撞到的 Run/Work 上限;批准一次即同时放行两层,不再连问两次。`resume` 校验批准仍有效后逐条落 `BUDGET_EXTENDED`,并把整份放行计划钉在第一条上:放行落到一半进程被杀,再次 `resume` 按钉住的计划补齐剩余项,不从已被抬高的状态重算。过期批准、新请求不继承旧 grants。会话在 worktree 里手修并用 `resume --adopt <sha>` 回答该请求时,候选虽换成新提交,仍继承请求上的 grants(grants 属于这一轮,不属于某个候选;计划变了则不继承),修完重验后直接进入下一轮 review,不再二次停车。
|
|
57
57
|
- 防止自定义 workflow 的成功循环失控:同一步总 attempt 达到有效上限(配置预算 + 人批扩额)的 **3 倍**后,在下一次执行前仍以 kind `budget` 兜底停人。成功/infra 退款不增加兜底上限;批准扩额会提高它。
|
|
58
58
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
**一轮一问**:review 发现阻断问题且下一轮会超 Run 或 Work 上限时,提前停 `enter-fix`;有分诊用 `finding-triage`,无分诊用 `budget`。批准覆盖「修复 + 重新验证 + 再审一轮」,所需扩额随请求的可选 `grants` 落账,Run/Work 同时到顶只问一次;拒绝结束本 Run,由人按现有证据决定是否合并。批准旧的 `enter-review` / `resume-review` 预算停车时也同时放行已到顶的另一层上限。新候选或过期批准不能沿用旧请求的 grants。
|
|
22
22
|
|
|
23
|
-
**按收敛止损**:上限(`reviewRoundsPerWork`,默认 6,跨本 Work 所有 Run 累计)之内,review 只在不收敛时停 `enter-fix`(kind `review-not-converging`)
|
|
23
|
+
**按收敛止损**:上限(`reviewRoundsPerWork`,默认 6,跨本 Work 所有 Run 累计)之内,review 只在不收敛时停 `enter-fix`(kind `review-not-converging`):同一个问题修过又出现(同指纹,或 reviewer 换了说法但引用同一组文件、描述相近;已 dismiss 的不算),或本轮阻断数多于上一轮。批准即修复 + 重验 + 再审一轮,不动预算;阻断 finding 都是新的、数量不多于上一轮时自动继续。停在这里时先给用户看又出现的指纹,问是继续修、dismiss,还是按现有证据结束本 Run。
|
|
24
24
|
|
|
25
25
|
## 6. 三个仪式(防腐烂的关键,缺了机制必朽)
|
|
26
26
|
|
package/lessons.md
CHANGED
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
|
|
89
89
|
**根因**:把三种本应分开的东西揉成一个开关:① 每次提交都该跑的机器检查;② 实现中突发的高风险语义 delta;③ 里程碑候选的完整一致性核查。再用「任务结束」「文件数」而不是**风险状态是否变化 / 候选 hash 是否变化**做触发条件,任务拆得越细,审查次数反而越多;P2 也被当成重新跑全流程的理由。流程于是优化了审查产量,没有优化风险发现率。
|
|
90
90
|
|
|
91
|
-
**解药**:review 只在 verify 通过后对固定 candidate
|
|
91
|
+
**解药**:review 只在 verify 通过后对固定 candidate 跑一次,轮数只有一个上限 `reviewRoundsPerWork`(默认 6,跨本 Work 所有 Run 累计),上限之内只在不收敛时停人(见第 23 条);`cache.verify: tree` 让同树同命令的证据直接复用(标 `REUSED`);reviewer 输入带 `lastReviewed` 与历史裁决 `anchor`,只核 delta;P0/P1 阻断即派 fixer,高风险项目可开 `reviewTriage: required` 先人分诊,P2 默认挂账。通用原则:**核查强度跟风险变化走,不跟任务数和文档行数走;写者≠审者保留,重复全审不是独立性。**
|
|
92
92
|
|
|
93
93
|
## 15. 追踪项被当成任务边界,人不断说“继续”和“批准”
|
|
94
94
|
|
|
@@ -143,3 +143,9 @@
|
|
|
143
143
|
**症状**:候选已合入生产的 Work 因最后一个 Run 是 CANCELLED 而显示 `STOPPED_CANCELLED`,`next:` 催重试;四个已关窗的 release 车道 Work 显示 `MERGE_READY`、"nothing to merge";已合并的 Work 仍报 14 条未裁决 finding;`doctor` 通过两次,`start` 都被"plan 未镜像到子仓"的 policy 挡在 build;驾驶会话在 worktree 里手修并提交后,只能批准 enter-fix 让 fixer 空跑再多一次 verify(前端 Run 因此 verify 5 次、fix 3 次)。
|
|
144
144
|
**根因**:阶段判定只看最后一个 Run 的终态,不看候选是否已在主干;overview 不认识 release 车道;doctor 检查的是配置合法性,不是 start 第一道门会读的事实;内核只认 worker 产出的候选,没有"人供候选"的入口。
|
|
145
145
|
**解药**:候选合入主干即 `MERGED`(哪怕最新 Run 取消),release 车道成功关窗即 `RELEASED`,已合并/已发布/已关闭不再提示未裁决数;`doctor` 打印本仓 intent/plan 存在与接受状态,并按 policy 预告 start 会停在哪一步;`resume --adopt <sha>` 以人为 actor 钉候选、从 verify 续跑。**同批发现**:仓内 `.buildbeat/worktrees/` 会被 vitest 等按文件系统收集的框架当成测试目录(模板 gitignore 与指南补排除);`start` 被仓锁挡住时只说"another run is active",现在打印持锁 Run 与 `status` 命令,锁本身未放开。通用原则:**任何"到哪了"的读数都必须从事实(主干、车道、门)推导,而不是从最后一条记录的状态字段抄。**
|
|
146
|
+
|
|
147
|
+
## 23. 中途审批点被驾驶会话自己批,同一个问题换个说法就认不出
|
|
148
|
+
|
|
149
|
+
**症状**:每个 Run 默认 2 轮 review 封顶,加上样板默认开分诊门,正常要修两三轮的 Run 一次次停在 `enter-fix` / `resume-review` 等人。一个仓最近 10 个 Run 中途停人 9 次,7 次是分诊门,9 次全部由驾驶会话自己批准,人只在合并决定时出现过一次。改成「按收敛止损」后,判断「修过的问题又出现」靠 finding 指纹(严重度 + 描述原文),但 reviewer 的描述带 `文件:行号`、措辞每轮微调:回放 4 个仓 161 轮 review,按指纹一次重复都没有,而同一个「diff 含允许范围外的文件」在一个 Run 里被连审三轮,只是第二轮写成「仍包含」、第三轮写成「包含本次允许范围之外的改动」。
|
|
150
|
+
**根因**:审批点按「步数」设,而不是按「需要人判断的事」设——会话能自己批的门,停下来只是延迟;另一边,用来做裁决台账的精确指纹被拿去做「是不是同一个问题」的模糊判断,两个用途要求相反(裁决要精确、收敛判断要容忍改写)。
|
|
151
|
+
**解药**:review 轮数只留一个按 Work 计的上限(默认 6),样板默认不开分诊门,上限之内只在不收敛时停人(`review-not-converging`);收敛判断用独立的 `sameIssue`:同指纹,或引用同一组文件且描述相近,或描述高度相近(字符二元组 Dice ≥ 0.5 / 0.6;严重度不参与、只差行号也算,描述去掉文件与数字后不足 16 字只认这两种精确情况),指纹与裁决不变。阈值来自那 161 轮回放:真实改写全部命中,无关 finding 最高 0.57、同文件无关 finding 最高 0.33。通用原则:**审批点要么真的要人判断,要么不设;同一份标识不要同时承担「精确去重」和「模糊归并」两种职责。**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haiyangbg/buildbeat",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.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": {
|
package/src/v2/cli/run.js
CHANGED
|
@@ -345,9 +345,9 @@ function loadRunConfig(flags, command) {
|
|
|
345
345
|
if (config.stallAfterMs !== undefined && !(Number(config.stallAfterMs) > 0)) {
|
|
346
346
|
throw new Error(`stallAfterMs must be a positive number, got: ${config.stallAfterMs}`);
|
|
347
347
|
}
|
|
348
|
-
// budgets: run config beats the preset (
|
|
349
|
-
//
|
|
350
|
-
//
|
|
348
|
+
// budgets: run config beats the preset (a preset cap that could not be
|
|
349
|
+
// raised per run once ended a pilot's shipped candidates as CANCELLED
|
|
350
|
+
// runs).
|
|
351
351
|
const budgets = {};
|
|
352
352
|
if (config.budgets !== undefined) {
|
|
353
353
|
if (!config.budgets || typeof config.budgets !== "object" || Array.isArray(config.budgets)) {
|
|
@@ -38,6 +38,76 @@ export function fingerprintFinding(finding) {
|
|
|
38
38
|
return createHash("sha256").update(normalized, "utf8").digest("hex").slice(0, 16);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// File anchors a summary cites ("src/a.js:12", "README.md"), line numbers
|
|
42
|
+
// dropped, sorted and de-duplicated.
|
|
43
|
+
const ANCHOR_PATTERN = /(?:[\w.@-]+\/)*[\w@-][\w.@-]*\.[A-Za-z][A-Za-z0-9]{0,5}(?=[::\s,,、;;.。))\]】`'"“”‘’>]|$)/g;
|
|
44
|
+
const SOURCE_EXTENSION = /\.(?:c|cc|cpp|cs|css|go|h|html|java|js|json|jsx|kt|md|mjs|py|rb|rs|scss|sh|sql|swift|toml|ts|tsx|vue|xml|ya?ml)$/i;
|
|
45
|
+
|
|
46
|
+
function fileAnchors(summary) {
|
|
47
|
+
const found = (summary.match(ANCHOR_PATTERN) ?? [])
|
|
48
|
+
.filter((path) => path.includes("/") || SOURCE_EXTENSION.test(path));
|
|
49
|
+
return [...new Set(found)].sort().join("|");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function issueText(summary) {
|
|
53
|
+
return summary
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.replace(ANCHOR_PATTERN, "")
|
|
56
|
+
.replace(/[\d\s\p{P}\p{S}]+/gu, "");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Dice coefficient over character bigrams; works the same for Chinese and
|
|
60
|
+
// English and needs no tokenizer.
|
|
61
|
+
function similarity(left, right) {
|
|
62
|
+
if (left.length < 2 || right.length < 2) return left === right ? 1 : 0;
|
|
63
|
+
const counts = new Map();
|
|
64
|
+
for (let i = 0; i < left.length - 1; i += 1) {
|
|
65
|
+
const pair = left.slice(i, i + 2);
|
|
66
|
+
counts.set(pair, (counts.get(pair) ?? 0) + 1);
|
|
67
|
+
}
|
|
68
|
+
let shared = 0;
|
|
69
|
+
for (let i = 0; i < right.length - 1; i += 1) {
|
|
70
|
+
const pair = right.slice(i, i + 2);
|
|
71
|
+
const remaining = counts.get(pair) ?? 0;
|
|
72
|
+
if (remaining > 0) {
|
|
73
|
+
shared += 1;
|
|
74
|
+
counts.set(pair, remaining - 1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return (2 * shared) / (left.length - 1 + right.length - 1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Whether two findings describe the same problem, for review convergence
|
|
81
|
+
// only: fingerprints stay exact so adjudications keep their meaning.
|
|
82
|
+
// Reviewers restate a finding they already reported ("still contains", new
|
|
83
|
+
// line numbers, a reworded tail), so equal fingerprints alone never matched
|
|
84
|
+
// across 161 replayed review rounds while the same problem came back three
|
|
85
|
+
// times. Thresholds from that replay: same anchors and >= 0.5, or >= 0.6
|
|
86
|
+
// otherwise, caught every restatement; unrelated pairs peaked at 0.57, and at
|
|
87
|
+
// 0.33 when they cited the same files. Severity never splits an issue, and
|
|
88
|
+
// the same description at moved line numbers is the same issue at any
|
|
89
|
+
// length. Below 16 characters of description bigrams say little
|
|
90
|
+
// ("issue two" / "issue three" share most of them), so a short summary
|
|
91
|
+
// matches only in those two exact ways.
|
|
92
|
+
const MIN_ISSUE_TEXT = 16;
|
|
93
|
+
|
|
94
|
+
function plainSummary(summary) {
|
|
95
|
+
return summary.toLowerCase().replace(/\s+/g, " ").trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function sameIssue(a, b) {
|
|
99
|
+
if (plainSummary(a.summary) === plainSummary(b.summary)) return true;
|
|
100
|
+
const left = issueText(a.summary);
|
|
101
|
+
const right = issueText(b.summary);
|
|
102
|
+
const anchors = fileAnchors(a.summary);
|
|
103
|
+
const sameAnchors = anchors !== "" && anchors === fileAnchors(b.summary);
|
|
104
|
+
if (sameAnchors && left === right) return true;
|
|
105
|
+
if (Math.min(left.length, right.length) < MIN_ISSUE_TEXT) return false;
|
|
106
|
+
const score = similarity(left, right);
|
|
107
|
+
if (sameAnchors && score >= 0.5) return true;
|
|
108
|
+
return score >= 0.6;
|
|
109
|
+
}
|
|
110
|
+
|
|
41
111
|
export function readFindingsAccount(repoRoot, workId) {
|
|
42
112
|
const filePath = accountPath(repoRoot, workId);
|
|
43
113
|
if (!existsSync(filePath)) {
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
latestAdjudications,
|
|
40
40
|
readFindingsAccount,
|
|
41
41
|
recordReviewFindings,
|
|
42
|
+
sameIssue,
|
|
42
43
|
} from "./findings.js";
|
|
43
44
|
import { resolveRepoRef, toRepoRef } from "./repo-ref.js";
|
|
44
45
|
|
|
@@ -333,10 +334,11 @@ function reviewRoundAttempt(ref, step) {
|
|
|
333
334
|
}
|
|
334
335
|
|
|
335
336
|
// Compares this round's blocking findings with the earlier review rounds of
|
|
336
|
-
// the same step in this run.
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
337
|
+
// the same step in this run. The same problem coming back after a fixer was
|
|
338
|
+
// sent (same fingerprint, or a restatement: sameIssue), or more blocking
|
|
339
|
+
// findings than last round, means another automatic round would not
|
|
340
|
+
// converge; a shrinking set of new findings keeps going without asking
|
|
341
|
+
// anyone. Returns the stop reason, or null.
|
|
340
342
|
function reviewNotConverging(context, step, blockingFindings) {
|
|
341
343
|
const rounds = context.ledger.state.evidence.filter((item) =>
|
|
342
344
|
item.kind === "review" && reviewRoundAttempt(item.ref ?? "", step) !== null);
|
|
@@ -346,12 +348,18 @@ function reviewNotConverging(context, step, blockingFindings) {
|
|
|
346
348
|
);
|
|
347
349
|
const blocking = (round) => (round.findings ?? [])
|
|
348
350
|
.filter((finding) => finding.severity === "P0" || finding.severity === "P1")
|
|
349
|
-
.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const lastCount = new Set(blocking(rounds.at(-2))).size;
|
|
353
|
-
const current = [...new
|
|
354
|
-
const repeated = current.
|
|
351
|
+
.filter((finding) => adjudicated.get(fingerprintFinding(finding))?.action !== "dismiss");
|
|
352
|
+
const earlier = rounds.slice(0, -1).flatMap((round, index) =>
|
|
353
|
+
blocking(round).map((finding) => ({ finding, round: index + 1 })));
|
|
354
|
+
const lastCount = new Set(blocking(rounds.at(-2)).map(fingerprintFinding)).size;
|
|
355
|
+
const current = [...new Map(blockingFindings.map((finding) => [fingerprintFinding(finding), finding])).values()];
|
|
356
|
+
const repeated = current.flatMap((finding) => {
|
|
357
|
+
const match = earlier.find((item) => sameIssue(finding, item.finding));
|
|
358
|
+
if (!match) return [];
|
|
359
|
+
const now = fingerprintFinding(finding);
|
|
360
|
+
const before = fingerprintFinding(match.finding);
|
|
361
|
+
return [now === before ? now : `${now} restates round ${match.round} ${before}`];
|
|
362
|
+
});
|
|
355
363
|
if (repeated.length > 0) {
|
|
356
364
|
return `${step} is not converging: ${repeated.length} blocking finding(s) came back after fix (${repeated.join(", ")}); approving runs fix + re-verify + one more review round, dismissing a finding stops it blocking`;
|
|
357
365
|
}
|