@haiyangbg/buildbeat 3.2.1 → 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 +18 -0
- package/SKILL.md +5 -7
- package/docs/CAPABILITY-MATRIX.md +1 -1
- package/docs/RELEASING.md +5 -2
- package/docs/v2/SPEC-0001-events-v1.md +1 -1
- package/docs/v2/guide/01-quickstart.en.md +2 -2
- package/docs/v2/guide/01-quickstart.md +2 -2
- package/docs/v2/guide/02-workflow-guide.en.md +4 -2
- package/docs/v2/guide/02-workflow-guide.md +4 -2
- package/docs/v2/guide/07-approval-guide.en.md +2 -2
- package/docs/v2/guide/07-approval-guide.md +4 -3
- package/docs/v2/skill/03-collaboration-rules.md +1 -1
- package/docs/v2/skill/04-rhythm-and-rituals.md +3 -1
- package/docs/v2/skill/06-bootstrap-and-takeover.md +1 -1
- package/example/AGENTS.md +1 -1
- package/example/delivery/work/WORK-EXPORT-DATE-FILTER/run-config.yaml +1 -3
- package/example/delivery/work/WORK-EXPORT-DATE-FILTER/workflow.yaml +3 -5
- package/lessons.md +7 -1
- package/package.json +1 -1
- package/src/v2/cli/run.js +18 -15
- package/src/v2/presets/software-delivery.yaml +3 -5
- package/src/v2/runtime/decisions.js +16 -0
- package/src/v2/runtime/findings.js +70 -0
- package/src/v2/runtime/notify.js +1 -1
- package/src/v2/runtime/orchestrator.js +62 -9
- package/src/v2/runtime/overview.js +2 -1
- package/templates/v2/AGENTS.md +1 -1
- package/templates/v2/run-config.example.yaml +5 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,26 @@
|
|
|
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
|
+
|
|
10
|
+
## v3.3.0 — 2026-10-02(review 少停人:收敛止损、单一轮数上限、模板关分诊、intent+plan 一次接受)
|
|
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
|
+
|
|
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 次批准都由驾驶会话自己批,人只出现在合并决定,审批点没起到人把关的作用。
|
|
15
|
+
- **review 轮数只剩一个上限**:`budgets.reviewRoundsPerWork` 不写时默认 6(此前不写即没有 Work 级上限);review 步没有显式 `maxAttempts` 时,每 Run 上限取同一个值,不会先于 Work 上限触发,官方预设不再自带 `maxAttempts.review`。`doctor` 显示 review 上限来源为 `reviewRoundsPerWork` 并标出默认值;run 配置样板删去 `maxAttempts.review`。显式写的 `maxAttempts.review` 照旧生效。原因:每 Run 与每 Work 两套计数让同一轮 review 要按两层上限解释和放行,止损线本来就按 Work 定。
|
|
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`,且全部由驾驶会话自己批准,没有起到人把关的作用。
|
|
17
|
+
- **intent 与 plan 一次接受**:`accept --artifact intent,plan` 一条命令接受多份工件,每份仍各记一行 `accept-<artifact>`、各自绑定 digest,policy 与台账格式不变;任一文件缺失则一份都不接受。`overview` 在 intent 未接受且 plan 待接受时提示 `--artifact intent,plan`;`SKILL.md` 改为给用户看完摘要后只问一次「接受」。原因:本仓每个 Work 的 intent 与 plan 都在约 60 毫秒内先后被接受,两次接受实际是一个决定。
|
|
18
|
+
|
|
5
19
|
## v3.2.1 — 2026-09-26(补丁:开关参数单写、drive() 拆分、信封脚本测试、英文指南)
|
|
6
20
|
|
|
21
|
+
> **发布状态**:`@haiyangbg/buildbeat@3.2.1` 已于 2026-09-26 从 `main`(PR #54 内容、release PR #55,merge commit `8d5f7bc`,tag `v3.2.1`)经 OIDC Trusted Publishing 发布到 dist-tag **`latest`**(run 36241955690,publish 与 verify 双 job 一次 success;所有者授权「推送并发 3.2.1 / 继续」)。独立回读(直连 npmjs.org):`latest` = 3.2.1、integrity 与发布前本地候选逐字一致、SLSA v1 provenance、隔离安装 `--version` = 3.2.1、裸调用零写入、已发布 CLI 接受单写 `--apply` / `--json`、包内 12 份英文指南在位且无 pilot / 历史文档、`npm audit signatures` 通过;GitHub Release v3.2.1 标 Latest,证据见 [`docs/releases/V3.2.1-RELEASE-EVIDENCE-2026-09-26.md`](docs/releases/V3.2.1-RELEASE-EVIDENCE-2026-09-26.md)。
|
|
22
|
+
>
|
|
23
|
+
> 同日所有者本人经 npm 两步验证把 dist-tag `next` 从 3.0.1 挪到 3.2.1(此前 `@next` 比 `@latest` 旧);直连 npmjs.org 回读 `next` = `latest` = 3.2.1。
|
|
24
|
+
|
|
7
25
|
- CLI 开关参数可单写:`--json`、`--apply`、`--force`、`--once` 不必再跟 `true`(旧写法照旧有效);缺值时报错点名参数(`--config needs a value`),多余的裸词报 `unexpected argument`。
|
|
8
26
|
- `drive()` 从 459 行的单一循环体拆成五个有名字的阶段函数(原代码按原顺序搬移,循环本身 19 行),review 步判定收成一处 `isReviewStep()`;行为零变化——未改动任何测试,全量通过。
|
|
9
27
|
- CodeQL 也分析推到 `v2` 的提交。
|
package/SKILL.md
CHANGED
|
@@ -28,7 +28,7 @@ description: BuildBeat —— 面向人和 AI 会话的工程交付工作流,上
|
|
|
28
28
|
| 「换会话」「删旧会话」「继续这个项目」「同事接手」 | 按 [接续指南](docs/v2/guide/11-session-handoff.md) 核对项目入口、Work、Git、`overview` / `inbox` / `status`;离开前补齐未落盘事实,接手后区分活动 Run / 中断 / 待批 / 终态 | 「已保存哪些上下文、工作停在哪、下一步」;不把删聊天当删工作树,不重启仍活动的 Run,不伪造或代批决定 |
|
|
29
29
|
| 「当前进度」「待办是什么」「X 上线了吗」「离上线还差多远」 | `buildbeat overview --repo .`(每个 Work 的阶段 + 下一步该谁 + `cost:` 已花的 Run/review 轮/等人次数/worker 时长)+ `observe status --repo .` | 每件事一句:走到哪、卡在谁、下一步;**不列命令**;花费超过 intent 止损线的 Work 要主动说「已 N 轮 review / N 小时,继续还是砍」 |
|
|
30
30
|
| 「有什么要我拍板」 | `buildbeat inbox --repo .` | 逐项:等什么、证据在哪、推荐 A/B;用户回「批准/拒绝」后会话调 `approve`/`reject` |
|
|
31
|
-
| 「开个 Work:〔目标〕」 | 写 `delivery/work/<ID>/intent.md`(为什么做 + **止损线**:最多几个 Run / 几轮 review / 几小时,越线先问人)+ `plan.md`(怎么做)+ `run-config.yaml`(`budgets.reviewRoundsPerWork` 对应止损线);给用户看摘要 |
|
|
31
|
+
| 「开个 Work:〔目标〕」 | 写 `delivery/work/<ID>/intent.md`(为什么做 + **止损线**:最多几个 Run / 几轮 review / 几小时,越线先问人)+ `plan.md`(怎么做)+ `run-config.yaml`(`budgets.reviewRoundsPerWork` 对应止损线);给用户看摘要 | 「看完说接受」;用户说一次「接受」→ `accept --artifact intent,plan`(一条命令,两份各自 digest 绑定;不要分两次问) |
|
|
32
32
|
| 「开工」「再来一轮」 | 先 `buildbeat doctor --config <run-config.yaml>`(会报本仓 intent/plan 是否存在且已接受、哪条 policy 会把 start 挡在哪步、每步预算),再 `start --config <run-config.yaml> --attempt new`(自动编号 RUN-X-01/02…,自动作废同 Work 的旧等待;**用 nohup/setsid 脱离启动**) | 「已起 RUN-X-02,停在合并决定时会通知/我会告诉你」 |
|
|
33
33
|
| 「怎么样了」「卡住了吗」「正常吗」 | `buildbeat status --repo . --run <RUN>` | 一句:在跑第几步、跑了多久、历史通常多久、最后一次输出几分钟前;`STALLED` 就说「疑似卡住,建议停/等」;停在 kind `infra` 就说「worker 环境/后端故障,不是代码问题,恢复后我重跑,预算不扣」 |
|
|
34
34
|
| 「批准 RUN-X」「拒绝,原因…」 | 先看 `inbox` 该 Run 等的是哪条 transition,再 `approve --transition <t> --by <用户名>` / `reject --reason`;非终态转换(`enter-fix` / `resume-<step>` / `enter-review`)批准后再 `resume --config <cfg>` 续跑;用 `--attempt new` 自动编号时会选择该家族唯一未终态 Run,也可 `--run <RUN-ID>` 指定;多个候选或没有未终态 Run 时按报错处理 | 说清批的是哪一步:「放行 fixer,续跑中」/「再跑一次,续跑中」/「合并决定已落,候选 <sha> 具备合并条件;合并/push/部署要你另说」。`SUCCEEDED` 不等于已合并 |
|
|
@@ -63,14 +63,12 @@ entry: build
|
|
|
63
63
|
allowedPaths:
|
|
64
64
|
- src
|
|
65
65
|
- tests
|
|
66
|
-
# P0/P1
|
|
67
|
-
reviewTriage:
|
|
66
|
+
# off = P0/P1 直接派 fixer;高风险项目改 required,每轮先停人分诊
|
|
67
|
+
reviewTriage: off
|
|
68
68
|
# 可省;run 配置 > 预设 > 默认。非只读步成功不扣次数;review 仍按轮计费。
|
|
69
|
-
# review
|
|
70
|
-
# reviewRoundsPerWork
|
|
69
|
+
# review 不收敛(修过的 finding 又出现/阻断数变多)才在 enter-fix 停人;
|
|
70
|
+
# reviewRoundsPerWork(默认 6)是 review 轮数唯一上限,跨本 Work 所有 Run 累计,到顶一次批准修复、重验、再审
|
|
71
71
|
budgets:
|
|
72
|
-
maxAttempts:
|
|
73
|
-
review: 2
|
|
74
72
|
reviewRoundsPerWork: 6
|
|
75
73
|
# 同树+同命令+同信封已通过就复用证据(标 REUSED)
|
|
76
74
|
cache:
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
| 能力 | Skill-only / 手工路径 | 运行时 `buildbeat` | 权威与边界 |
|
|
18
18
|
|---|---|---|---|
|
|
19
|
-
| 工件接受 | 会话记一行到 `decisions.jsonl` | `accept --artifact intent\|plan\|spec
|
|
19
|
+
| 工件接受 | 会话记一行到 `decisions.jsonl` | `accept --artifact intent\|plan\|spec`(逗号分隔可一次接受多份,如 `intent,plan`):每份各自 digest 绑定;改过即 `stale`,`doctor` / `overview` 报出 | 接受不是开工;policy 按 riskPreset 决定 build 前要求哪些工件已接受 |
|
|
20
20
|
| 自动闭环 | 无 | `start --config <run-config> [--attempt new]`:隔离 worktree、builder→verify→review→fix 自动路由、停 `WAITING_HUMAN` | 一仓同时只有一个活动 Run;worker 是配置的任意命令,内核不内置模型 |
|
|
21
21
|
| 进度与等待 | 会话读目录 | `overview`(每 Work 阶段 / 下一步 / 成本)、`inbox`(等人的 Run + 下一句命令)、`status`(步、耗时、STALLED、证据、findings)、`metrics` | 全部只读;本机绝对路径不进输出 |
|
|
22
22
|
| 人批 | 会话记一行 | `approve --transition <t>` / `reject`:绑定 transition + candidate + planDigest + evidenceDigest,盖章前重读实况;非终态转换后 `resume` | 合并决定 = 候选具备合并条件;合并 / push / 部署无调用路径(不变量 20) |
|
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.
|
|
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:
|
|
@@ -69,7 +71,7 @@ npm publish --access public --tag bootstrap --registry=https://registry.npmjs.or
|
|
|
69
71
|
npm view @haiyangbg/buildbeat dist-tags --json --registry=https://registry.npmjs.org/
|
|
70
72
|
```
|
|
71
73
|
|
|
72
|
-
The intended bootstrap readback is `bootstrap: 0.0.0` without a stable `latest` claim. During the first BuildBeat package creation, npm also attached `latest: 0.0.0` despite the explicit non-default tag and rejected the authenticated delete with HTTP 400. Treat any such first-package `latest` as temporary bootstrap exposure: do not activate launch documentation, do not republish or unpublish the immutable version, and complete the reviewed OIDC release so the real version takes over `latest`. The readback at the time of that first scoped release (2026-08-25) was `bootstrap: 0.0.0` and `latest: 1.21.0`;
|
|
74
|
+
The intended bootstrap readback is `bootstrap: 0.0.0` without a stable `latest` claim. During the first BuildBeat package creation, npm also attached `latest: 0.0.0` despite the explicit non-default tag and rejected the authenticated delete with HTTP 400. Treat any such first-package `latest` as temporary bootstrap exposure: do not activate launch documentation, do not republish or unpublish the immutable version, and complete the reviewed OIDC release so the real version takes over `latest`. The readback at the time of that first scoped release (2026-08-25) was `bootstrap: 0.0.0` and `latest: 1.21.0`; on 2026-09-05 `latest` moved to `2.0.0` with `next` at `2.0.0-beta.5`; the current tags are in the current-state paragraph at the top of this runbook. The bootstrap package page is registry-creation evidence only: it is not a BuildBeat release, is not tagged in Git, and has no provenance claim. The npm documentation requires 2FA or an allowed granular token for direct publication; no long-lived publish token belongs in Git, shell history, logs, or a chat transcript. See npm's guides for [scoped public packages](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages/) and [Trusted Publishing](https://docs.npmjs.com/trusted-publishers/).
|
|
73
75
|
|
|
74
76
|
## Independent readback
|
|
75
77
|
|
|
@@ -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 @@
|
|
|
71
71
|
| `FAILURE_FINGERPRINT` | kernel | `step, command, exitCode, errorDigest, diffDigest` | 无进展/相同失败检测的输入 |
|
|
72
72
|
| `BUDGET_CONSUMED` | kernel | `kind ∈ {attempts,tokens,cost,time}, amount, remaining` | 预算台账(卡点 1:token/费用不再 `UNVERIFIED`) |
|
|
73
73
|
| `BUDGET_EXTENDED` | kernel | `step, amount, maxAttempts, approvalRef, scope?` | additive(迭代 09):人批准了预算耗尽的 `resume-<step>`,该步上限 +`amount`;状态 `budgetExtensions[step]` 累加,`maxAttemptsFor(step)` 据此重放。`scope: work` 时是 Work 级 review 轮数上限(`budgets.reviewRoundsPerWork`)被人放行一轮,累加到 `workReviewGrants` |
|
|
74
|
-
| `HUMAN_REQUESTED` | kernel | `transition, subject{candidate,planDigest,evidenceDigest}, reasons`;additive:`kind ∈ {boundary,final-decision,finding-triage,stale,infra}` | 进入 WAITING_HUMAN |
|
|
74
|
+
| `HUMAN_REQUESTED` | kernel | `transition, subject{candidate,planDigest,evidenceDigest}, reasons`;additive:`kind ∈ {boundary,final-decision,finding-triage,stale,infra,budget,work-review-cap,review-not-converging}`(`review-not-converging`:review 修过的 finding 又出现或阻断数多于上一轮,停 `enter-fix`) | 进入 WAITING_HUMAN |
|
|
75
75
|
| `DECISION_RECORDED` | human | `decision ∈ {approved,rejected}, transition, subject, decisionRef`;additive(迭代 09):`adopted?: <sha>`、`resumeAt?: <step>`(adopt 时 subject 即该提交,恢复从 `resumeAt` 起而非 transition 所指的步) | 同步落 Git 决策记录 |
|
|
76
76
|
| `APPROVAL_STALE` | kernel | `approvalRef, changed ⊆ {candidate,plan,evidence}` | F6 的机器化 |
|
|
77
77
|
| `CHECKPOINT` | kernel | `resumePoint{step,attempt}, workspaceStates[]` | F5 的机器化:恢复只允许从最近 CHECKPOINT 或安全推导点继续 |
|
|
@@ -47,7 +47,7 @@ entry: build
|
|
|
47
47
|
allowedPaths:
|
|
48
48
|
- src
|
|
49
49
|
- tests
|
|
50
|
-
reviewTriage:
|
|
50
|
+
reviewTriage: off
|
|
51
51
|
envelope:
|
|
52
52
|
prompts: ../../envelope/prompts
|
|
53
53
|
workers:
|
|
@@ -91,7 +91,7 @@ workers:
|
|
|
91
91
|
- `workers.<role>` is any CLI: to switch tools, change only what follows `--` (`claude -p`, any script), see the [Adapter guide](04-adapter-guide.md) (Chinese); the reviewer's output format is in the [Worker contract](05-worker-contract.md) (Chinese) and already spelled out in the prompt template.
|
|
92
92
|
- **`fixer` is not optional**: without it, a failed verify or a blocking review stops the Run at `WAITING_HUMAN` (reason `no adapter configured for worker fixer`) waiting for you to fix by hand; nothing gets fixed automatically.
|
|
93
93
|
- Worker subprocesses receive only `PATH HOME LANG LC_ALL TMPDIR TERM USER SHELL` by default; inject anything else by name with `env:` ([Adapter guide](04-adapter-guide.md), Chinese).
|
|
94
|
-
- `reviewTriage:
|
|
94
|
+
- `reviewTriage: off` (the default) sends P0/P1 findings straight to the fixer, and review rounds stay bounded by `budgets`. Set it to `required` to see every round before a fixer runs, which suits high-risk projects.
|
|
95
95
|
|
|
96
96
|
## 3. Accept the plan
|
|
97
97
|
|
|
@@ -47,7 +47,7 @@ entry: build
|
|
|
47
47
|
allowedPaths:
|
|
48
48
|
- src
|
|
49
49
|
- tests
|
|
50
|
-
reviewTriage:
|
|
50
|
+
reviewTriage: off
|
|
51
51
|
envelope:
|
|
52
52
|
prompts: ../../envelope/prompts
|
|
53
53
|
workers:
|
|
@@ -91,7 +91,7 @@ workers:
|
|
|
91
91
|
- `workers.<角色>` 是任意 CLI:换工具只改 `--` 后面的命令(`claude -p`、任意脚本都行),见 [Adapter 指南](04-adapter-guide.md);reviewer 的输出格式见 [Worker 合同](05-worker-contract.md),prompt 模板已写明。
|
|
92
92
|
- **`fixer` 不是可选项**:没配它,verify 失败或 review 阻断时 Run 会停 `WAITING_HUMAN`(理由 `no adapter configured for worker fixer`)等你手修,不会自动修。
|
|
93
93
|
- worker 子进程默认只拿到 `PATH HOME LANG LC_ALL TMPDIR TERM USER SHELL`;需要别的变量用 `env:` 点名注入([Adapter 指南](04-adapter-guide.md))。
|
|
94
|
-
- `reviewTriage:
|
|
94
|
+
- `reviewTriage: off`(默认)时 P0/P1 finding 直接派 fixer,review 轮数仍受 `budgets` 上限约束。想每轮都先过你的手,改成 `required`,适合高风险项目。
|
|
95
95
|
|
|
96
96
|
## 3. 接受计划
|
|
97
97
|
|
|
@@ -69,7 +69,9 @@ The run config can also declare (beta.3, all from real incidents in a thirty-rou
|
|
|
69
69
|
|
|
70
70
|
## Review round budget
|
|
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
|
+
|
|
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.
|
|
73
75
|
|
|
74
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).
|
|
75
77
|
|
|
@@ -85,7 +87,7 @@ budgets:
|
|
|
85
87
|
|
|
86
88
|
`doctor` prints each step's effective limit and where it comes from (run config / workflow preset / default).
|
|
87
89
|
|
|
88
|
-
**Review rounds counted per Work (iteration 09)**: a per-Run budget cannot stop "a new Run for every round" — one pilot Work ran 21 Runs and 9 review rounds without the 2-round cap ever triggering. `budgets.reviewRoundsPerWork: N` makes the kernel count, before a review step starts, the review rounds of **every** Run of this Work (superseded ones and ones already compacted into a run-record included); at N it stops at `WAITING_HUMAN` (kind `work-review-cap`, transition `enter-review`): approving grants one more review round (ledger `BUDGET_EXTENDED scope=work`), rejecting means merging or closing on the evidence at hand. Each Work in `overview` gets a line `cost: review rounds · findings · human waits · worker time`, and the run-record carries a `cost` block — read that line before deciding "continue or cut"; the stop-loss line in the intent (at most so many Runs / review rounds / hours) is checked against it.
|
|
90
|
+
**Review rounds counted per Work (iteration 09)**: a per-Run budget cannot stop "a new Run for every round" — one pilot Work ran 21 Runs and 9 review rounds without the 2-round cap ever triggering. `budgets.reviewRoundsPerWork: N` (N = 6 when unset) makes the kernel count, before a review step starts, the review rounds of **every** Run of this Work (superseded ones and ones already compacted into a run-record included); at N it stops at `WAITING_HUMAN` (kind `work-review-cap`, transition `enter-review`): approving grants one more review round (ledger `BUDGET_EXTENDED scope=work`), rejecting means merging or closing on the evidence at hand. Each Work in `overview` gets a line `cost: review rounds · findings · human waits · worker time`, and the run-record carries a `cost` block — read that line before deciding "continue or cut"; the stop-loss line in the intent (at most so many Runs / review rounds / hours) is checked against it.
|
|
89
91
|
|
|
90
92
|
## Worktrees live inside the repository: keep `.buildbeat/` out of test collection
|
|
91
93
|
|
|
@@ -69,7 +69,9 @@ run 配置还可声明(beta.3,皆来自三十轮部署战役的真实事故
|
|
|
69
69
|
|
|
70
70
|
## review 轮数预算
|
|
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
|
+
|
|
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` 停车,同样附上不收敛的理由。
|
|
73
75
|
|
|
74
76
|
预算耗尽后停的那次 `resume-<step>`,**人批准即多给一次**:内核落一条 `BUDGET_EXTENDED`(台账事实,可重放),该步上限 +1 再跑;拒绝即终止 Run。此前批准只会让同一请求立刻回来(试点两条应用登录 Run 因此以 CANCELLED 收场,候选却已在生产)。
|
|
75
77
|
|
|
@@ -85,7 +87,7 @@ budgets:
|
|
|
85
87
|
|
|
86
88
|
`doctor` 打印每步生效的上限与来源(run config / workflow preset / default)。
|
|
87
89
|
|
|
88
|
-
**按 Work 累计的 review 轮数(迭代 09)**:每 Run 的预算挡不住"每轮一个新 Run"——试点一个 Work 跑了 21 个 Run、9 轮 review,2 轮封顶从未触发。`budgets.reviewRoundsPerWork: N
|
|
90
|
+
**按 Work 累计的 review 轮数(迭代 09)**:每 Run 的预算挡不住"每轮一个新 Run"——试点一个 Work 跑了 21 个 Run、9 轮 review,2 轮封顶从未触发。`budgets.reviewRoundsPerWork: N`(不写时 N = 6)让内核在 review 步起跑前统计本 Work **所有** Run(含已作废、含已压成 run-record 的)的 review 轮数,达到 N 即停 `WAITING_HUMAN`(kind `work-review-cap`,transition `enter-review`):批准即再审一轮(台账 `BUDGET_EXTENDED scope=work`),拒绝则按手头证据合并或关闭。`overview` 每个 Work 多一行 `cost: review rounds · findings · human waits · worker 时长`,run-record 也带 `cost` 块——"继续还是砍"之前先看这一行;intent 里的止损线(最多几个 Run / 几轮 review / 几小时)就对着它核。
|
|
89
91
|
|
|
90
92
|
## 工作树在仓内:把 `.buildbeat/` 排除出测试收集
|
|
91
93
|
|
|
@@ -33,8 +33,8 @@ Sessions and documents used to mix "accept / approve / resume / succeeded / merg
|
|
|
33
33
|
|
|
34
34
|
| Word | Command | Meaning | Is not |
|
|
35
35
|
|---|---|---|---|
|
|
36
|
-
| **Accept** | `accept --artifact intent\|plan` | A human endorses
|
|
37
|
-
| **Approve a transition** | `approve --transition <t>` | Lets the Run take **this one** transition: `enter-fix` (release the fixer after triage
|
|
36
|
+
| **Accept** | `accept --artifact intent\|plan`, or `--artifact intent,plan` for both at once | A human endorses each artifact's digest, one record per artifact; editing an artifact makes only its acceptance `stale` | Starting work; it creates no Run |
|
|
37
|
+
| **Approve a transition** | `approve --transition <t>` | Lets the Run take **this one** transition: `enter-fix` (release the fixer after triage or after a review that is not converging, kind `review-not-converging`; when the review budget is spent, this one approval also grants the next review round, and so does answering it with a hand fix via `resume --adopt <sha>`), `resume-<step>` (run the step once more after a budget or infra stop; records `BUDGET_EXTENDED`; successful build / verify / fix attempts are not charged, and a budget stop that hits both the run and the work review cap is lifted by one approval), `enter-review` (one more review round after the Work-level cap), `enter-apply-readback` ("I have done it" on the release lane) | Approving any other transition; after a non-terminal transition is approved the Run **does not move by itself**: `resume --config <run-config>` continues it (the `next:` line printed by `approve` says so) |
|
|
38
38
|
| **Merge decision** (final approval) | `approve --transition enter-wait-merge` | The candidate is fit to merge: candidate + planDigest + evidenceDigest all hold at this instant; the Run reaches the terminal state `SUCCEEDED` and the run-record is compacted into the Git plane | Code merged, pushed or deployed: those three remain your actions outside the Runner, always |
|
|
39
39
|
| **Run SUCCEEDED** | — | The Run stopped where it should and the evidence is complete | The Work is finished. `overview` shows `MERGED` only after reading back that the candidate is on the current branch |
|
|
40
40
|
| **Reject** | `reject --reason` | The Run ends (`FAILED`, reason recorded) | The artifacts are invalidated; the acceptance state of intent/plan does not change |
|
|
@@ -33,8 +33,8 @@ buildbeat accept --repo . --work WORK-X --artifact plan --by <名字> # 工
|
|
|
33
33
|
|
|
34
34
|
| 词 | 命令 | 含义 | 不等于 |
|
|
35
35
|
|---|---|---|---|
|
|
36
|
-
| **接受**(accept) | `accept --artifact intent\|plan` |
|
|
37
|
-
| **批准某转换**(approve) | `approve --transition <t>` | 允许 Run 走**这一条** transition:`enter-fix
|
|
36
|
+
| **接受**(accept) | `accept --artifact intent\|plan`,或 `--artifact intent,plan` 一次接受两份 | 每份工件的 digest 各自被人认可、各记一行;改过哪份哪份 `stale` | 开工;不产生任何 Run |
|
|
37
|
+
| **批准某转换**(approve) | `approve --transition <t>` | 允许 Run 走**这一条** transition:`enter-fix`(分诊后或 review 不收敛时放行 fixer;附带 grants 时还放行重验后的下一轮 review)、`resume-<step>`(预算耗尽后扩额,或 infra 退款后重试)、`enter-review`(Work 级 review 上限后再审一轮)、`enter-apply-readback`(上线车道"我做完了") | 批准了别的转换;非终态转换批准后 Run **不会自己动**,要 `resume --config <run-config>` 续跑(`approve` 输出的 `next:` 行会写明) |
|
|
38
38
|
| **合并决定**(最终批准) | `approve --transition enter-wait-merge` | 候选已具备合并条件:candidate + planDigest + evidenceDigest 此刻全部成立;Run 进终态 `SUCCEEDED`,run-record 压进 Git 面 | 代码已合并、已 push、已部署——这三件永远是你在 Runner 之外的动作 |
|
|
39
39
|
| **Run SUCCEEDED** | — | Run 停在了它该停的地方,证据齐 | Work 完成。`overview` 只有回读到候选在当前分支上才显示 `MERGED` |
|
|
40
40
|
| **拒绝**(reject) | `reject --reason` | Run 终止(`FAILED`,理由入账) | 工件失效;intent/plan 的接受状态不变 |
|
|
@@ -50,8 +50,9 @@ buildbeat accept --repo . --work WORK-X --artifact plan --by <名字> # 工
|
|
|
50
50
|
## 预算停车:成功不扣次数,一轮一问
|
|
51
51
|
|
|
52
52
|
- 非只读步(build / verify / fix)成功的 attempt 不扣 `maxAttempts`,只读步仍消耗次数,review 按轮计费。`STEP_FINISHED.free: true` 与 `BUDGET_CONSUMED.amount: 0` 记录退款;没有新字段的旧台账保持原有回放结果。
|
|
53
|
-
- 真失败到顶仍停 `resume-<step>`,同指纹两次仍停,release 预设的 `maxAttempts: 1` 仍有效。infra
|
|
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`:同一个问题在修过之后又出现(同指纹,或换了说法但引用同一组文件、描述相近;已 dismiss 的不算),或本轮阻断数多于上一轮。kind 为 `review-not-converging`(有分诊时仍是 `finding-triage`,理由里多一条不收敛),请求不带 grants,批准不动预算。阻断 finding 都是新的、数量不多于上一轮时自动继续。见 [Workflow 指南](02-workflow-guide.md) 的「按收敛止损」。
|
|
55
56
|
- 每一次预算停车(`enter-fix`、`resume-<step>`、Work 级 `enter-review`)都在请求上记可选 `grants`,列出下一次执行会撞到的 Run/Work 上限;批准一次即同时放行两层,不再连问两次。`resume` 校验批准仍有效后逐条落 `BUDGET_EXTENDED`,并把整份放行计划钉在第一条上:放行落到一半进程被杀,再次 `resume` 按钉住的计划补齐剩余项,不从已被抬高的状态重算。过期批准、新请求不继承旧 grants。会话在 worktree 里手修并用 `resume --adopt <sha>` 回答该请求时,候选虽换成新提交,仍继承请求上的 grants(grants 属于这一轮,不属于某个候选;计划变了则不继承),修完重验后直接进入下一轮 review,不再二次停车。
|
|
56
57
|
- 防止自定义 workflow 的成功循环失控:同一步总 attempt 达到有效上限(配置预算 + 人批扩额)的 **3 倍**后,在下一次执行前仍以 kind `budget` 兜底停人。成功/infra 退款不增加兜底上限;批准扩额会提高它。
|
|
57
58
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
3. **交接靠 candidate hash + 台账**:Run 停在合并决定时 candidate 已由 Git 回读固定,`resume --adopt <sha>` 要求树干净且 HEAD 就是该 sha;hash 不得编造。
|
|
12
12
|
4. **护栏与不可逆动作**:开工 `overview`;部署/改契约/migration 等不可逆动作前再核一次并走人批;exit 0 不消除 `warning/unverified`。
|
|
13
13
|
5. **风险分轨**:Risk Preset 决定人批点(§5);别用牛刀杀鸡,也别借 `fast` 绕过高风险 delta 的独立核查。
|
|
14
|
-
6. **核查门**:Run 内 reviewer 只读、结构化 findings;`reviewTriage: required` 时 P0/P1 先过人分诊再派 fixer;review
|
|
14
|
+
6. **核查门**:Run 内 reviewer 只读、结构化 findings;`reviewTriage: required` 时 P0/P1 先过人分诊再派 fixer;review 不收敛(修过的 finding 又出现,或阻断数多于上一轮)即停人,每 Work 默认 6 轮兜底(`reviewRoundsPerWork`)。**完成 = hash + 可核验证据**;证据分 L0 声称 / L1 `文件:行` / L2 编译·类型 / L3 自动化测试 / L4 线上实测,`standard` 最低 L3,上线必须 L4;`UNVERIFIED` 永不当作通过。
|
|
15
15
|
7. **状态单点**:事实进 Run 证据与 Work 记录;进度看 `overview`,度量看 `metrics`。
|
|
16
16
|
8. **视觉问题带图对比**:提 UI bug 必附『实现截图 ⟷ 设计稿截图』并排 + 标注差异点。
|
|
17
17
|
9. **单点事实**:线上版本只信实查(`observe status` / 部署平台),任何文档不写「当前线上 vX」;每个收敛后的真实决策包只在 `pm/decisions.md` 记一行;历史台账不回改。
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
机器闸(gitleaks pre-commit)、证据制与合并候选一次核查任何预设都不跳;高风险 delta 不得借 `fast` 绕过独立核查。预算耗尽是停人不是失败。非只读步(build / verify / fix)成功不扣 `maxAttempts`,只读步仍按尝试次数计费,review 按轮计费;基础设施故障(超时 / 崩溃 / 非 JSON / exit 75)判 `infra` 停人、不派 fixer、不扣预算。真失败到顶仍停 `resume-<step>`,批准多给一次。同一步总 attempt 达到有效预算上限(配置值 + 人批扩额)的 3 倍后,下一次执行前以 kind `budget` 兜底停人,防止成功循环失控;退款不抬高该兜底上限。
|
|
20
20
|
|
|
21
|
-
**一轮一问**:review 发现阻断问题且下一轮会超 Run 或 Work 上限时,提前停 `enter-fix`;有分诊用 `finding-triage`,无分诊用 `budget`。批准覆盖「修复 + 重新验证 + 再审一轮」,所需扩额随请求的可选 `grants` 落账,Run/Work 同时到顶只问一次;拒绝结束本 Run,由人按现有证据决定是否合并。批准旧的 `enter-review` / `resume-review` 预算停车时也同时放行已到顶的另一层上限。新候选或过期批准不能沿用旧请求的 grants
|
|
21
|
+
**一轮一问**:review 发现阻断问题且下一轮会超 Run 或 Work 上限时,提前停 `enter-fix`;有分诊用 `finding-triage`,无分诊用 `budget`。批准覆盖「修复 + 重新验证 + 再审一轮」,所需扩额随请求的可选 `grants` 落账,Run/Work 同时到顶只问一次;拒绝结束本 Run,由人按现有证据决定是否合并。批准旧的 `enter-review` / `resume-review` 预算停车时也同时放行已到顶的另一层上限。新候选或过期批准不能沿用旧请求的 grants。
|
|
22
|
+
|
|
23
|
+
**按收敛止损**:上限(`reviewRoundsPerWork`,默认 6,跨本 Work 所有 Run 累计)之内,review 只在不收敛时停 `enter-fix`(kind `review-not-converging`):同一个问题修过又出现(同指纹,或 reviewer 换了说法但引用同一组文件、描述相近;已 dismiss 的不算),或本轮阻断数多于上一轮。批准即修复 + 重验 + 再审一轮,不动预算;阻断 finding 都是新的、数量不多于上一轮时自动继续。停在这里时先给用户看又出现的指纹,问是继续修、dismiss,还是按现有证据结束本 Run。
|
|
22
24
|
|
|
23
25
|
## 6. 三个仪式(防腐烂的关键,缺了机制必朽)
|
|
24
26
|
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
- [ ] 4. 第一个 Work:`delivery/work/<WORK-ID>/` 写 `intent.md`(为什么 + 止损线)、`plan.md`;`templates/v2/run-config.example.yaml` → `run-config.yaml`(改 work / run / allowedPaths / verifier / 把 `--` 后的工具命令换成用户实际用的);`$(npm root -g)/@haiyangbg/buildbeat/src/v2/presets/software-delivery.yaml` → `workflow.yaml`
|
|
57
57
|
- [ ] 5. 通知(问题 E):要就写 `.buildbeat/notify.yaml`,URL 只能来自环境变量;不要就在收尾说明"等待只在 inbox 里"。有生产环境就再放一份 `.buildbeat/observe.yaml`(只读探针)
|
|
58
58
|
- [ ] 6. 机器闸:各代码仓装 gitleaks pre-commit(`command -v gitleaks` 查无则提醒安装,并记入收尾报告);meta 仓 git init + 远端,代码子仓各自独立 git
|
|
59
|
-
- [ ] 7. 首跑验收:用户说「接受」→ `accept --artifact intent`
|
|
59
|
+
- [ ] 7. 首跑验收:用户说「接受」→ `accept --artifact intent,plan`(一次接受两份);`buildbeat doctor --config …` 全段读一遍(intent/plan 接受状态、env 姿态、预算、start 会停在哪);`start --config … --attempt new`(脱离启动)→ 停 `WAITING_HUMAN`;`overview` / `status` 把候选、verify 退出码、findings 读给用户。**这一次 Run 停在合并决定之前,不宣布"接入完成"**
|
|
60
60
|
- [ ] 8. 收尾一屏:生成了什么 / 默认拿主意的项 / 首跑停在哪、证据在哪 / 下一步由谁做(合并是人的动作)
|
|
61
61
|
```
|
|
62
62
|
|
package/example/AGENTS.md
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
**③ 交接靠 candidate hash + 台账** —— Run 停在合并决定时 candidate 已由 Git 回读固定;跨会话接力读 `delivery/work/<id>/` 即知全部事实,hash 不得编造。
|
|
45
45
|
**④ 护栏与不可逆动作** —— 开工 `overview`;部署/改契约/migration 等不可逆动作前再核一次并走人批;exit 0 不消除 `warning/unverified`。
|
|
46
46
|
**⑤ 风险分轨** —— Risk Preset:`fast`(仅 merge 人批)/ `standard`(plan+merge,默认)/ `controlled`(intent+plan+merge+release)/ `release`(上线回读车道)。
|
|
47
|
-
**⑥ 核查门** —— Run 内 reviewer 只读、结构化 findings;`reviewTriage: required` 时 P0/P1 先过人分诊再派 fixer;review
|
|
47
|
+
**⑥ 核查门** —— Run 内 reviewer 只读、结构化 findings;`reviewTriage: required` 时 P0/P1 先过人分诊再派 fixer;review 不收敛(修过的 finding 又出现,或阻断数多于上一轮)即停人,每 Work 默认 6 轮兜底(`reviewRoundsPerWork`)。**完成 = hash + 可核验证据**;标准轨最低 L3,上线必须 L4。`UNVERIFIED` 永不当作通过。
|
|
48
48
|
**⑦ 状态单点** —— 事实进 Run 证据与 Work 记录;进度看 `overview`,度量看 `metrics`(本地只读)。
|
|
49
49
|
**⑧ 视觉问题带图对比** —— 提 UI bug 必附『实现截图 ⟷ 设计稿截图』并排 + 标注差异点。
|
|
50
50
|
**⑨ 单点事实** —— 线上版本只信实查(`observe status` / 部署平台);任何文档不写「当前线上 vX」;每个收敛后的真实决策包只在 `pm/decisions.md` 记一行;历史台账不回改。
|
|
@@ -37,8 +37,6 @@ transitions:
|
|
|
37
37
|
to: fix
|
|
38
38
|
terminal:
|
|
39
39
|
- wait-merge
|
|
40
|
-
# Review rounds are capped
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
maxAttempts:
|
|
44
|
-
review: 2
|
|
40
|
+
# Review rounds are capped per work (run config budgets.reviewRoundsPerWork,
|
|
41
|
+
# default 6); a round whose blocking findings come back after fix, or
|
|
42
|
+
# outnumber the last round's, stops for a human before the cap.
|
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
|
+
"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
|
@@ -21,7 +21,7 @@ import { loadWorkflow, nextStep } from "../engine/workflow.js";
|
|
|
21
21
|
import { parseYamlSubset } from "../engine/yaml-subset.js";
|
|
22
22
|
import { parsePolicyDoc } from "../policy/policy.js";
|
|
23
23
|
import { observeStatus, runObserveCycle, triageIntent } from "../observe/observe.js";
|
|
24
|
-
import {
|
|
24
|
+
import { acceptArtifacts, adoptCandidate, approveRun, listInbox, rejectRun } from "../runtime/decisions.js";
|
|
25
25
|
import { checkRequires } from "../runtime/env-contract.js";
|
|
26
26
|
import {
|
|
27
27
|
adjudicateFinding,
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
subscribes,
|
|
49
49
|
} from "../runtime/notify.js";
|
|
50
50
|
import { writeRunRecord } from "../runtime/run-record.js";
|
|
51
|
-
import { resumeRun, startRun } from "../runtime/orchestrator.js";
|
|
51
|
+
import { DEFAULT_REVIEW_ROUNDS_PER_WORK, resumeRun, startRun } from "../runtime/orchestrator.js";
|
|
52
52
|
import { toRepoRef } from "../runtime/repo-ref.js";
|
|
53
53
|
import { EventLedger } from "../storage/event-ledger.js";
|
|
54
54
|
import { acquireLock, listHeldRunLocks, releaseLock } from "../workspace/workspace-manager.js";
|
|
@@ -67,7 +67,7 @@ Usage:
|
|
|
67
67
|
buildbeat overview --repo <path> [--work <WORK-ID>] [--json]
|
|
68
68
|
buildbeat approve --repo <path> --run <RUN-ID> --transition <t> [--by <name>] [--config <run-config.yaml>]
|
|
69
69
|
buildbeat reject --repo <path> --run <RUN-ID> [--transition <t>] [--reason <text>] [--by <name>]
|
|
70
|
-
buildbeat accept --repo <path> --work <WORK-ID> --artifact <plan|intent|spec> [--by <name>]
|
|
70
|
+
buildbeat accept --repo <path> --work <WORK-ID> --artifact <plan|intent|spec>[,<more>] [--by <name>]
|
|
71
71
|
buildbeat doctor --config <run-config.yaml>
|
|
72
72
|
buildbeat events --repo <path> --run <RUN-ID>
|
|
73
73
|
buildbeat replay --repo <path> --run <RUN-ID>
|
|
@@ -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)) {
|
|
@@ -718,12 +718,15 @@ function commandAccept(flags) {
|
|
|
718
718
|
if (!flags.repo || !flags.work || !flags.artifact) {
|
|
719
719
|
throw new Error("accept requires --repo, --work and --artifact");
|
|
720
720
|
}
|
|
721
|
-
const
|
|
721
|
+
const artifacts = String(flags.artifact).split(",").map((name) => name.trim()).filter(Boolean);
|
|
722
|
+
const results = acceptArtifacts(resolve(flags.repo), flags.work, artifacts, {
|
|
722
723
|
by: flags.by ?? "human",
|
|
723
724
|
});
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
725
|
+
for (const result of results) {
|
|
726
|
+
console.log(`accepted ${result.artifact} as ${result.decisionRef}`);
|
|
727
|
+
console.log(` digest: ${result.digest}`);
|
|
728
|
+
}
|
|
729
|
+
console.log(" note: editing an artifact after acceptance makes its acceptance stale");
|
|
727
730
|
}
|
|
728
731
|
|
|
729
732
|
// Artifacts a policy rule requires to be accepted (artifact.accepted leaves
|
|
@@ -791,20 +794,20 @@ function commandDoctor(flags) {
|
|
|
791
794
|
}
|
|
792
795
|
console.log("kernel capabilities: merge/deploy/publish have no call path in the runner (invariant 20)");
|
|
793
796
|
const budgetLines = [];
|
|
797
|
+
const reviewRounds = options.budgets.reviewRoundsPerWork ?? DEFAULT_REVIEW_ROUNDS_PER_WORK;
|
|
794
798
|
for (const step of options.workflow.steps) {
|
|
795
799
|
if (!step.worker) {
|
|
796
800
|
continue;
|
|
797
801
|
}
|
|
798
802
|
const fromRun = options.budgets.maxAttempts?.[step.id];
|
|
799
803
|
const fromPreset = options.workflow.budgets?.maxAttempts?.[step.id];
|
|
800
|
-
const
|
|
801
|
-
const
|
|
804
|
+
const review = step.id === "review" || step.worker === "reviewer";
|
|
805
|
+
const effective = fromRun ?? fromPreset ?? (review ? reviewRounds : options.maxAttemptsPerStep);
|
|
806
|
+
const source = fromRun !== undefined ? "run config" : fromPreset !== undefined ? "workflow preset" : review ? "reviewRoundsPerWork" : "default";
|
|
802
807
|
budgetLines.push(`${step.id}=${effective} (${source})`);
|
|
803
808
|
}
|
|
804
809
|
console.log(`budgets (maxAttempts per step; approving resume-<step> after exhaustion grants +1): ${budgetLines.join(", ")}`);
|
|
805
|
-
|
|
806
|
-
console.log(`budgets.reviewRoundsPerWork: ${options.budgets.reviewRoundsPerWork} (counted across every run of the work, superseded ones included)`);
|
|
807
|
-
}
|
|
810
|
+
console.log(`budgets.reviewRoundsPerWork: ${reviewRounds}${options.budgets.reviewRoundsPerWork === undefined ? " (default)" : ""} (counted across every run of the work, superseded ones included)`);
|
|
808
811
|
// Same preconditions start's first gate will read (real incident, twice:
|
|
809
812
|
// doctor passed, start stopped at build because plan.md was not mirrored
|
|
810
813
|
// into the repository the run was started in).
|
|
@@ -37,8 +37,6 @@ transitions:
|
|
|
37
37
|
to: fix
|
|
38
38
|
terminal:
|
|
39
39
|
- wait-merge
|
|
40
|
-
# Review rounds are capped
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
maxAttempts:
|
|
44
|
-
review: 2
|
|
40
|
+
# Review rounds are capped per work (run config budgets.reviewRoundsPerWork,
|
|
41
|
+
# default 6); a round whose blocking findings come back after fix, or
|
|
42
|
+
# outnumber the last round's, stops for a human before the cap.
|
|
@@ -294,6 +294,22 @@ export function acceptArtifact(repoRoot, workId, artifact, { by = "human", ts }
|
|
|
294
294
|
return { decisionRef, digest };
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
// One human "accept" for several artifacts: every file must exist before any
|
|
298
|
+
// acceptance is written, and each artifact keeps its own digest-bound line.
|
|
299
|
+
export function acceptArtifacts(repoRoot, workId, artifacts, options = {}) {
|
|
300
|
+
const unique = [...new Set(artifacts)];
|
|
301
|
+
if (unique.length === 0) {
|
|
302
|
+
throw new DecisionError("no artifact to accept");
|
|
303
|
+
}
|
|
304
|
+
const missing = unique
|
|
305
|
+
.map((artifact) => join(repoRoot, "delivery", "work", workId, `${artifact}.md`))
|
|
306
|
+
.filter((filePath) => !existsSync(filePath));
|
|
307
|
+
if (missing.length > 0) {
|
|
308
|
+
throw new DecisionError(`artifact file missing: ${missing.join(", ")}`);
|
|
309
|
+
}
|
|
310
|
+
return unique.map((artifact) => ({ artifact, ...acceptArtifact(repoRoot, workId, artifact, options) }));
|
|
311
|
+
}
|
|
312
|
+
|
|
297
313
|
export function rejectRun(repoRoot, runId, { by = "human", transition, reason, ts } = {}) {
|
|
298
314
|
acquireLock(repoRoot, runId);
|
|
299
315
|
try {
|
|
@@ -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)) {
|
package/src/v2/runtime/notify.js
CHANGED
|
@@ -82,7 +82,7 @@ export function nextReply({ repoLabel, state }) {
|
|
|
82
82
|
}
|
|
83
83
|
const runId = state.run.id;
|
|
84
84
|
const lines = [];
|
|
85
|
-
if (pending.kind === "finding-triage") {
|
|
85
|
+
if (pending.kind === "finding-triage" || pending.kind === "review-not-converging") {
|
|
86
86
|
lines.push(`buildbeat findings list --repo ${repoLabel} --work ${state.run.work}`);
|
|
87
87
|
lines.push(
|
|
88
88
|
`buildbeat findings adjudicate --repo ${repoLabel} --work ${state.run.work} --fingerprint <fp> --action accept|dismiss --by <you>`,
|
|
@@ -39,10 +39,12 @@ 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
|
|
|
45
46
|
const KERNEL = { kind: "kernel", id: "orchestrator" };
|
|
47
|
+
export const DEFAULT_REVIEW_ROUNDS_PER_WORK = 6;
|
|
46
48
|
|
|
47
49
|
export class OrchestratorError extends Error {
|
|
48
50
|
constructor(message) {
|
|
@@ -225,11 +227,16 @@ function makeContext(options, ledger, workspace) {
|
|
|
225
227
|
// granted on this ledger adds to it. Real incident: the preset's two
|
|
226
228
|
// review rounds could not be raised from the run config, and approving
|
|
227
229
|
// resume-review re-asked the same question forever.
|
|
230
|
+
// A review step without an explicit cap takes the work-level cap as its run
|
|
231
|
+
// cap, so review rounds are counted in one unit: the work.
|
|
228
232
|
context.runBudgets = options.budgets ?? {};
|
|
233
|
+
context.reviewRoundsPerWork = context.runBudgets.reviewRoundsPerWork ?? DEFAULT_REVIEW_ROUNDS_PER_WORK;
|
|
229
234
|
context.budgetLimitFor = (step) =>
|
|
230
235
|
(context.runBudgets.maxAttempts?.[step] ??
|
|
231
236
|
workflow.budgets?.maxAttempts?.[step] ??
|
|
232
|
-
|
|
237
|
+
(isReviewStep(step, workflow.steps.find((item) => item.id === step))
|
|
238
|
+
? context.reviewRoundsPerWork
|
|
239
|
+
: maxAttemptsPerStep)) +
|
|
233
240
|
(ledger.state.budgetExtensions?.[step] ?? 0);
|
|
234
241
|
context.maxAttemptsFor = (step) => context.budgetLimitFor(step) +
|
|
235
242
|
(ledger.state.steps[step]?.infraAttempts ?? 0) +
|
|
@@ -297,8 +304,8 @@ function budgetReasons(context, step, safeguard = false) {
|
|
|
297
304
|
|
|
298
305
|
function workReviewBudget(context, step) {
|
|
299
306
|
const stepDef = context.workflow.steps.find((item) => item.id === step);
|
|
300
|
-
const cap = context.
|
|
301
|
-
if (
|
|
307
|
+
const cap = context.reviewRoundsPerWork;
|
|
308
|
+
if (!isReviewStep(step, stepDef)) return null;
|
|
302
309
|
const prior = computeWorkCost(context.repoRoot, context.ledger.state.run.work, {
|
|
303
310
|
excludeRun: context.ledger.state.run.id,
|
|
304
311
|
});
|
|
@@ -319,6 +326,49 @@ function budgetGrants(context, step) {
|
|
|
319
326
|
return grants;
|
|
320
327
|
}
|
|
321
328
|
|
|
329
|
+
function reviewRoundAttempt(ref, step) {
|
|
330
|
+
const name = ref.split(/[\\/]/).pop();
|
|
331
|
+
if (!name.startsWith(`${step}-`)) return null;
|
|
332
|
+
const match = /^(\d+)\.json$/.exec(name.slice(step.length + 1));
|
|
333
|
+
return match ? Number(match[1]) : null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Compares this round's blocking findings with the earlier review rounds of
|
|
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.
|
|
342
|
+
function reviewNotConverging(context, step, blockingFindings) {
|
|
343
|
+
const rounds = context.ledger.state.evidence.filter((item) =>
|
|
344
|
+
item.kind === "review" && reviewRoundAttempt(item.ref ?? "", step) !== null);
|
|
345
|
+
if (rounds.length < 2) return null;
|
|
346
|
+
const adjudicated = latestAdjudications(
|
|
347
|
+
readFindingsAccount(context.repoRoot, context.ledger.state.run.work),
|
|
348
|
+
);
|
|
349
|
+
const blocking = (round) => (round.findings ?? [])
|
|
350
|
+
.filter((finding) => finding.severity === "P0" || finding.severity === "P1")
|
|
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
|
+
});
|
|
363
|
+
if (repeated.length > 0) {
|
|
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`;
|
|
365
|
+
}
|
|
366
|
+
if (current.length > lastCount) {
|
|
367
|
+
return `${step} is not converging: ${current.length} blocking finding(s) this round, ${lastCount} last round; approving runs fix + re-verify + one more review round`;
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
322
372
|
// Evaluates configured policies of `type` for `appliesTo`, records every
|
|
323
373
|
// verdict as a POLICY_EVALUATED event, and reports what the kernel must do.
|
|
324
374
|
// ADVISORY failures are recorded but never gate (doctor reports the gap).
|
|
@@ -886,8 +936,8 @@ function recordStepResult(context, step, stepDef, attempt, { before, outputPath
|
|
|
886
936
|
}
|
|
887
937
|
|
|
888
938
|
// Settles the outcome and picks the next step; blocking findings stop once
|
|
889
|
-
// for triage
|
|
890
|
-
// next step, or null when the run stopped.
|
|
939
|
+
// for triage, the review budget or a review that is not converging before
|
|
940
|
+
// any fixer runs. Returns the next step, or null when the run stopped.
|
|
891
941
|
function routeAfterStep(context, step, stepDef, { stepStatus, blockingFindings, tree, exec }) {
|
|
892
942
|
let outcome;
|
|
893
943
|
if (stepStatus !== "succeeded") {
|
|
@@ -901,9 +951,11 @@ function routeAfterStep(context, step, stepDef, { stepStatus, blockingFindings,
|
|
|
901
951
|
// Ask before spending fix/verify workers: one approval covers the next
|
|
902
952
|
// round and both review caps, with the grant bound to this request.
|
|
903
953
|
if (routed && outcome === "findings-blocking") {
|
|
904
|
-
const
|
|
954
|
+
const review = isReviewStep(step, stepDef);
|
|
955
|
+
const grants = review ? budgetGrants(context, step) : [];
|
|
956
|
+
const stalled = review ? reviewNotConverging(context, step, blockingFindings) : null;
|
|
905
957
|
const triage = context.reviewTriage === "required";
|
|
906
|
-
if (triage || grants.length) {
|
|
958
|
+
if (triage || grants.length || stalled) {
|
|
907
959
|
const { used, limit, failures } = budgetUsage(context, step);
|
|
908
960
|
const workBudget = workReviewBudget(context, step);
|
|
909
961
|
context.waitHuman(
|
|
@@ -912,12 +964,13 @@ function routeAfterStep(context, step, stepDef, { stepStatus, blockingFindings,
|
|
|
912
964
|
...(grants.length ? [
|
|
913
965
|
`${step} budget exhausted: ${used}/${limit} review round(s) used in this run${workBudget ? `, ${workBudget.rounds}/${workBudget.allowed} across the work` : ""}, ${failures} real failure(s); approve enter-${routed} = fix + re-verify + one more review round; reject = end this run and decide the merge on the evidence you have`,
|
|
914
966
|
] : []),
|
|
967
|
+
...(stalled ? [stalled] : []),
|
|
915
968
|
`review found ${blockingFindings.length} blocking finding(s); ${triage ? "triage" : "approve another round"} before ${routed} runs`,
|
|
916
969
|
...blockingFindings.slice(0, 5).map((finding) =>
|
|
917
970
|
`[${finding.severity} ${fingerprintFinding(finding)}] ${finding.summary.slice(0, 200)}`),
|
|
918
971
|
`adjudicate fingerprints (findings adjudicate), then approve enter-${routed} or reject the run`,
|
|
919
972
|
],
|
|
920
|
-
triage ? "finding-triage" : "budget",
|
|
973
|
+
triage ? "finding-triage" : grants.length ? "budget" : "review-not-converging",
|
|
921
974
|
grants,
|
|
922
975
|
);
|
|
923
976
|
return null;
|
|
@@ -1287,7 +1340,7 @@ export function resumeRun(options) {
|
|
|
1287
1340
|
amount: 1,
|
|
1288
1341
|
...(grant.scope === "work" ? { scope: "work" } : {}),
|
|
1289
1342
|
maxAttempts: grant.scope === "work"
|
|
1290
|
-
?
|
|
1343
|
+
? context.reviewRoundsPerWork + (ledger.state.workReviewGrants ?? 0) + 1
|
|
1291
1344
|
: context.maxAttemptsFor(grant.step) + 1,
|
|
1292
1345
|
approvalRef: approval.decisionRef,
|
|
1293
1346
|
grants: plan,
|
|
@@ -196,7 +196,8 @@ export function computeOverview(repoRoot, { work = null, repoLabel = "." } = {})
|
|
|
196
196
|
: `buildbeat accept --repo ${repoLabel} --work ${workId} --artifact intent --by <you>`;
|
|
197
197
|
} else if (!plan.accepted) {
|
|
198
198
|
stage = plan.stale ? "PLAN_STALE" : "PLAN_DRAFT";
|
|
199
|
-
|
|
199
|
+
const artifacts = intent.accepted ? "plan" : "intent,plan";
|
|
200
|
+
next = `buildbeat accept --repo ${repoLabel} --work ${workId} --artifact ${artifacts} --by <you>${plan.stale ? " # plan changed since acceptance" : ""}`;
|
|
200
201
|
} else {
|
|
201
202
|
stage = "READY_TO_RUN";
|
|
202
203
|
const configs = readdirSync(workDir).filter((name) => /^run-config.*\.ya?ml$/.test(name));
|
package/templates/v2/AGENTS.md
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
**③ 交接靠 candidate hash + 台账** —— Run 停在合并决定时 candidate 已由 Git 回读固定;跨会话接力读 `delivery/work/<id>/` 即知全部事实,hash 不得编造。
|
|
45
45
|
**④ 护栏与不可逆动作** —— 开工 `overview`;部署/改契约/migration 等不可逆动作前再核一次并走人批;exit 0 不消除 `warning/unverified`。
|
|
46
46
|
**⑤ 风险分轨** —— Risk Preset:`fast`(仅 merge 人批)/ `standard`(plan+merge,默认)/ `controlled`(intent+plan+merge+release)/ `release`(上线回读车道)。
|
|
47
|
-
**⑥ 核查门** —— Run 内 reviewer 只读、结构化 findings;`reviewTriage: required` 时 P0/P1 先过人分诊再派 fixer;review
|
|
47
|
+
**⑥ 核查门** —— Run 内 reviewer 只读、结构化 findings;`reviewTriage: required` 时 P0/P1 先过人分诊再派 fixer;review 不收敛(修过的 finding 又出现,或阻断数多于上一轮)即停人,每 Work 默认 6 轮兜底(`reviewRoundsPerWork`)。**完成 = hash + 可核验证据**;标准轨最低 L3,上线必须 L4。`UNVERIFIED` 永不当作通过。
|
|
48
48
|
**⑦ 状态单点** —— 事实进 Run 证据与 Work 记录;进度看 `overview`,度量看 `metrics`(本地只读)。
|
|
49
49
|
**⑧ 视觉问题带图对比** —— 提 UI bug 必附『实现截图 ⟷ 设计稿截图』并排 + 标注差异点。
|
|
50
50
|
**⑨ 单点事实** —— 线上版本只信实查(`observe status` / 部署平台);任何文档不写「当前线上 vX」;每个收敛后的真实决策包只在 `pm/decisions.md` 记一行;历史台账不回改。
|
|
@@ -14,14 +14,12 @@ entry: build
|
|
|
14
14
|
allowedPaths:
|
|
15
15
|
- src
|
|
16
16
|
- tests
|
|
17
|
-
# P0/P1 finding
|
|
18
|
-
reviewTriage:
|
|
19
|
-
# 非只读步成功不扣次数;review
|
|
20
|
-
#
|
|
21
|
-
#
|
|
17
|
+
# off = P0/P1 finding 直接派 fixer;高风险项目改成 required,每轮先停人分诊再派 fixer
|
|
18
|
+
reviewTriage: off
|
|
19
|
+
# 非只读步成功不扣次数;review 按轮计费。review 不收敛(修过的 finding 又出现,或阻断数多于上一轮)才在 enter-fix 停人。
|
|
20
|
+
# reviewRoundsPerWork 是 review 轮数唯一的上限(默认 6),跨本 Work 所有 Run 累计,对应 intent 的止损线;
|
|
21
|
+
# 到顶的阻断 review 在 enter-fix 一次批准修复、重验、再审。总 attempt 超过(配置值 + 人批扩额)的 3 倍前仍兜底停人。
|
|
22
22
|
budgets:
|
|
23
|
-
maxAttempts:
|
|
24
|
-
review: 2
|
|
25
23
|
reviewRoundsPerWork: 6
|
|
26
24
|
# 默认一个仓库同时只驱动一个 Run。确认本项目的测试不抢固定端口、不共用数据库后,
|
|
27
25
|
# 可加 parallel: true,让本 Work 的 Run 与其他同样打开开关的 Work 并行(同一 Work 仍互斥)
|