@haiyangbg/buildbeat 4.1.0 → 4.2.0

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.0 — 2026-10-07(次版本:合并决定点能接修复;合并批准绑定候选)
6
+
7
+ - 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
+ - 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
+ - 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.
10
+
5
11
  ## v4.1.0 — 2026-10-07(次版本:跨仓总览;提示里的命令与路径都能照做)
6
12
 
13
+ > **发布状态**:`@haiyangbg/buildbeat@4.1.0` 已于 2026-10-07 从 `main`(release PR #74,merge commit `9259ac9`,tag `v4.1.0`)经 OIDC Trusted Publishing 发布到 dist-tag **`latest`**(run 37590027989,publish 与 verify 双 job 一次 success;所有者授权「推送并发 4.1.0」)。独立回读(直连 npmjs.org):`latest` = 4.1.0、`dist.integrity` 与本地用 Node 24 + npm 11.19.0 打的候选逐字一致、注册表 tarball 解压后与候选逐字节相同、SLSA v1 provenance、隔离安装 `--version` = 4.1.0、裸调用零写入、用法含 `--all-repos`、`npm audit signatures` 通过;GitHub Release v4.1.0 标 Latest;本机两份全局安装均为 4.1.0;证据见 [`docs/releases/V4.1.0-RELEASE-EVIDENCE-2026-10-07.md`](docs/releases/V4.1.0-RELEASE-EVIDENCE-2026-10-07.md)。`next` 仍为 4.0.0。
14
+
7
15
  - Add read-only `status --all-repos`: discover the main repository, configured target checkouts and immediate child repositories, deduplicate real paths, show pending decisions first and count settled work. JSON retains all work; `--work` filters across repositories, while `--run` requires single-repository status.
8
16
  - Fix main-repository status/overview reporting target-repository work as ready to start: read state, findings and decisions from the target, retain the main run configuration for commands, and quote paths relative to the caller. Missing/invalid targets warn without hiding other work. Ship `src/v2/runtime/overview-repos.js` and update the package-file inventory for this runtime helper.
9
17
  - Hints give commands that work as written. A merged work whose run configs were all read and none has a `release:` section is told there is nothing left to do, instead of being sent to `buildbeat release`, which fails without that section and left the work unable to close; with a `release:` section the hint names the config that has it. A waiting run with no reply commands points to `status --work`, and `check` names `status` instead of the 3.x `inbox`. The run config template comment (and its copies in the Skill, both quickstarts and the example) names `check` / `run` / `--new`, and the quickstart says where the npm package installs the templates. Found by a first run of a fresh project on 4.0.0 with real codex workers.
package/SKILL.md CHANGED
@@ -11,11 +11,13 @@ description: BuildBeat 用项目文件接续上下文,在隔离工作树内自
11
11
 
12
12
  先跑 `buildbeat --version`,按结果选命令:
13
13
 
14
- - 4.x:按下文操作。
14
+ - 4.x:按下文操作,但合并决定点的两种修复入口有下述版本限制。
15
15
  - 3.x:下文的 `run`、`status --work`、`decide`、`check`、`history` 和 `work.md` 在 3.x 不存在,沿用项目现有的 3.x 写法:`intent.md` + `plan.md`,分别 `accept --artifact intent` 与 `accept --artifact plan`;`start --config <config> --attempt new` 开工,`resume --config <config> [--run <RUN>] [--adopt <sha> --by <name>]` 续跑;`overview`、`inbox`、`status --run <RUN>` 看进度;`approve --transition <t>` / `reject --reason <why>` 决定;`findings adjudicate` 裁决问题;`doctor` 检查配置;run-config 保留 `workflow:` 与 `riskPreset:`。
16
16
  - 3.x 的活动 Run 只能用 3.x 运行时完成或取消。升级到 4.x 前先问用户,并按[迁移说明](docs/MIGRATION.md)处理。
17
17
  - 没有运行时:按[不装运行时也能参与](docs/v2/guide/12-without-runtime.md)只读状态、写 `work.md` 和规定格式的决定行;不得代批 Run,不得声称验证、审查或回读已通过。
18
18
 
19
+ 合并决定点的 `run --adopt`、`decide --action fix` 与批准命令的 `--candidate` 候选绑定要求运行时 4.2 或更高版本。
20
+
19
21
  ## 会话操作
20
22
 
21
23
  | 用户意图 | 操作 |
@@ -24,9 +26,10 @@ description: BuildBeat 用项目文件接续上下文,在隔离工作树内自
24
26
  | 准备一项工作 | 写 `delivery/work/<ID>/work.md`,包含目标、范围、验收、实施计划;复制 run-config 样板并填真实命令 |
25
27
  | 接受工作说明 | `buildbeat accept --repo . --work <ID> --by <owner>`;用户已有明确授权时直接记录,不重复问同一个决定 |
26
28
  | 开工、继续 | `buildbeat run --config <config>`;首次编号,已有运行则恢复;明确新一轮时加 `--new` |
27
- | 批准、拒绝 | 读取状态卡,再 `buildbeat decide --repo . --run <RUN> --action approve|reject --transition <t> --by <owner>`;非终态决定后续跑 |
29
+ | 批准、拒绝 | 读取状态卡并复制批准或拒绝命令;合并批准为 `buildbeat decide --repo . --run <RUN> --action approve --transition enter-wait-merge --candidate <完整 SHA> --by <owner>`,使用卡片候选;修复后旧候选或未绑定候选的批准失效;非终态决定后续跑 |
30
+ | 合并决定退回修复 | `buildbeat decide --repo . --run <RUN> --action fix --reason <要修什么> --by <owner>`;仅合并决定点且有 fixer 时可用,再 `buildbeat run --config <config> --run <RUN>` 续跑;原因记为已接受 P1,重验、再审,review 预算照常累计 |
28
31
  | 问题接受、驳回 | `buildbeat decide --repo . --work <ID> --action accept|dismiss --fingerprint <fp> --by <owner> --note <reason>` |
29
- | 已手修并提交 | `buildbeat run --config <config> --run <RUN> --adopt <sha> --by <name>`;检查干净工作树与实际 HEAD 后从 verify 继续 |
32
+ | 已手修并提交 | `buildbeat run --config <config> --run <RUN> --adopt <sha> --by <name>`;检查干净工作树与实际 HEAD 后从 verify 继续;合并决定点也可用,此时须为当前候选的新后代且改动不越界 |
30
33
  | 配置或环境有问题 | `buildbeat check --config <config>`;显式 `--step <step>` 会在主检出执行 worker,只有用户任务需要才调用,结果不是正式证据 |
31
34
  | 上线后回读、关窗 | 人完成上线后 `buildbeat release --config <config> [--note <text>]`;最近一次回读通过再 `buildbeat decide --repo . --work <ID> --action close --result <text> --by <owner>`。回读失败不能关窗 |
32
35
  | 结束一轮 | `buildbeat stop --repo . --run <RUN> --reason <reason>`;活动驱动持锁时先处理进程,不能把 stop 当作进程杀手 |
@@ -1,6 +1,6 @@
1
1
  # BuildBeat capability matrix / 能力矩阵
2
2
 
3
- Current release: 4.1.0. One product path: Skill → local runtime →
3
+ Current release: 4.2.0. 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;
@@ -13,7 +13,7 @@ may be written by hand.
13
13
  | Delivery loop | Fixed build/verify/review/fix flow | External CLI tools do the actual work |
14
14
  | Recovery | Run/step ledger, resume, manual candidate adoption, stale locks | Active runtime is not transferred by Git clone |
15
15
  | Trusted result | Real command evidence, current candidate, read-only review | Script tests do not prove real AI business capability |
16
- | Decisions | Acceptance, exact approval, frozen checks, budget/convergence stops | No automatic merge, push or deployment |
16
+ | Decisions | Acceptance, exact approval bound to the candidate, frozen checks, budget/convergence stops, repairs at the merge decision (adopt a hand fix or send it back to the fixer) | No automatic merge, push or deployment |
17
17
  | Efficiency | Verify cache, review range hints, parallel Work isolation | External test state must be isolated and stable |
18
18
  | Feedback | Unified status/cost/decisions, also across a main repository and its code repositories (`--all-repos`), liveness, webhook/DingTalk | Notifications do not approve |
19
19
  | UI evidence | `requireScreenshot`: screenshots from verify, bound to the candidate and required at merge | The project's verify renders and captures |
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.1.0`; latest independently verified BuildBeat npm distribution `@haiyangbg/buildbeat@4.0.0` (dist-tags `latest` and `next`; the owner moved `next` from `3.3.1` to `4.0.0` with npm 2FA on 2026-10-05), anchored by annotated tag `v4.0.0` at commit `c15fbe0`, workflow run [37262387333](https://github.com/HaiYangBG1/BuildBeat/actions/runs/37262387333), and 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.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/`.
6
6
 
7
7
  ## Channels and branches
8
8
 
@@ -6,6 +6,17 @@ New work has one work.md. accept binds its current digest but does not start exe
6
6
 
7
7
  Status presents the transition, candidate, plan digest, evidence, reasons and next action. decide --action approve|reject must name the run and transition. Approval re-reads the candidate and frozen safeguards; changed subjects, dirty trees, missing evidence or unacceptable reviews cannot be stamped. Continue nonterminal decisions with run. Final approval means merge-ready only.
8
8
 
9
+ At the merge decision, `run --adopt`, `decide --action fix` and the approval command’s `--candidate` binding require runtime 4.2 or later.
10
+
11
+ When a problem is found at the merge decision (`enter-wait-merge`), repair it within the same Run and verify and review again:
12
+
13
+ - Manual repair: commit in the Run worktree, then execute `buildbeat run --config <config> --run <RUN> --adopt <sha> --by <name>`. The worktree must be clean, the SHA must be its actual HEAD and a new descendant of the current candidate, and changes must stay within `allowedPaths`. Resume at verify, review incrementally from the previous candidate, then wait for another merge decision.
14
+ - Return to the fixer: execute `buildbeat decide --repo <repo> --run <RUN> --action fix --reason <what to repair> --by <name>`, then continue with `buildbeat run --config <config> --run <RUN>`. This is available only at the merge decision with a configured fixer; otherwise commit a manual repair and adopt it. The reason becomes an accepted P1 in the Work findings account and is passed in full to the fixer.
15
+
16
+ After either repair path returns to the merge decision, read `buildbeat status --repo <repo> --run <RUN>` and copy its refreshed approval command: `buildbeat decide --repo <repo> --run <RUN> --action approve --transition enter-wait-merge --candidate <full SHA from the card> --by <name>`. The candidate is filled in by status, inbox and notifications. Approval with the old candidate, or without a candidate after repair, fails with `approval stale`; read the new card before deciding.
17
+
18
+ The repair decision answers the old request and preserves historical evidence; the merge check uses only evidence for the current candidate. Returning to fix does not spend a review round, grant budget or refund it. Subsequent reviews count normally. SUCCEEDED Runs cannot be reopened; active 3.x Runs still require their original runtime.
19
+
9
20
  Use decide --action accept|dismiss --work ... --fingerprint ... for findings. Exact-fingerprint dismissals persist; similarity detects nonconverging reviews and never substitutes for adjudication.
10
21
 
11
22
  Normal repairs continue automatically. Repeated issues, increased blockers, infrastructure failures and exhausted budgets create exception decisions. One budget grant covers its repair/reverification/review plan and can be replayed. The driving session cannot approve on behalf of the user without authorization.
@@ -6,6 +6,17 @@
6
6
 
7
7
  status 的决定卡包含 transition、候选、计划摘要、证据、原因和下一步。decide --action approve|reject 必须指向该 Run 与转换。批准前重新回读候选与冻结校验;变化、脏树、缺证据或不合格审查不能盖章。非终态批准后用 run 续跑,最终批准只表示具备合并条件。
8
8
 
9
+ 合并决定点的 `run --adopt`、`decide --action fix` 与批准命令的 `--candidate` 候选绑定要求运行时 4.2 或更高版本。
10
+
11
+ 合并决定点(`enter-wait-merge`)发现问题时,可以在本 Run 中修复并重新验证、审查:
12
+
13
+ - 手工修复:在 Run 工作树提交后执行 `buildbeat run --config <config> --run <RUN> --adopt <sha> --by <name>`。工作树必须干净,SHA 必须是实际 HEAD,并且是当前候选的新后代;改动仍受 `allowedPaths` 约束。从 verify 继续,review 使用上一候选的增量范围,最后再次等待合并决定。
14
+ - 退回 fixer:执行 `buildbeat decide --repo <repo> --run <RUN> --action fix --reason <要修什么> --by <name>`,再用 `buildbeat run --config <config> --run <RUN>` 续跑。仅合并决定点可用,且配置必须有 fixer;否则先手修再接纳。原因记为本 Work 已接受的 P1 问题,fixer 收到完整原因。
15
+
16
+ 任一修复路径回到合并决定后,先读 `buildbeat status --repo <repo> --run <RUN>`,复制刷新后的批准命令:`buildbeat decide --repo <repo> --run <RUN> --action approve --transition enter-wait-merge --candidate <卡片中的完整 SHA> --by <name>`。状态卡、待批列表和通知会填好候选;旧候选命令或修复后未携带候选的命令会报 `approval stale`,需要重新读取卡片再决定。
17
+
18
+ 修复决定回答旧请求,保留历史证据;合并检查只使用当前候选的证据。退回本身不计 review、不扩额也不退款,后续 review 照常累计。已批准的 SUCCEEDED Run 不重新打开;3.x 活动 Run 仍由原运行时处理。
19
+
9
20
  审查问题通过 decide --action accept|dismiss --work ... --fingerprint ... 裁决。精确指纹的驳回持续生效;换说法的相似性只用于停止不收敛循环,不代替人的裁决。
10
21
 
11
22
  正常修复自动继续;重复问题、阻断数增加、环境故障或预算耗尽才形成例外决定。预算批准一次放行对应修复、重验和再审;扩额记录可以重放,驾驶会话不能自行冒充用户批准。
@@ -2,8 +2,12 @@
2
2
 
3
3
  [简体中文](10-recovery.md)
4
4
 
5
+ At the merge decision, `run --adopt`, `decide --action fix` and the approval command’s `--candidate` binding require runtime 4.2 or later.
6
+
5
7
  - run resumes the unique nonterminal run in its family; choose --run when ambiguous. Archived work needs explicit --new to start another attempt.
6
8
  - An interrupted step reruns itself from the ledger. Handle a dirty worktree first. After committing a manual fix, --adopt checks the actual HEAD and continues at verify.
9
+ - The merge decision also accepts a manual repair: the new HEAD must descend from the candidate and stay in scope. Alternatively use `decide --action fix --repo <repo> --run <RUN> --reason <what to repair> --by <name>` with a configured fixer, then `run --config <config> --run <RUN>`. Both paths verify and review again before another merge decision, preserving review budgets and historical evidence.
10
+ - After repair, read the refreshed status card and copy its approval command with `--candidate <full SHA>`; old or unbound approvals fail with `approval stale`.
7
11
  - Reclaim a lock only when its owner is on this host and provably dead. Keep live, foreign-host and unidentified locks. stop is not a process-kill command.
8
12
  - Timeouts, crashes, malformed envelopes and exit 75 are infra: no fixer and no failure-budget charge. Decide and continue once the environment recovers.
9
13
  - A corrupt ledger exposes only its valid prefix and refuses appends. history --verify replays and validates without silently repairing it. Preserve the scene and candidate.
@@ -2,8 +2,12 @@
2
2
 
3
3
  [English](10-recovery.en.md)
4
4
 
5
+ 合并决定点的 `run --adopt`、`decide --action fix` 与批准命令的 `--candidate` 候选绑定要求运行时 4.2 或更高版本。
6
+
5
7
  - run 自动续接已有家族的唯一未终态 Run;多个候选时必须 --run 指定。历史工作需明确 --new 才会重新启动。
6
8
  - 中断步按台账重跑自身;脏工作树先处理。手修并提交后用 --adopt 指定实际 HEAD,从 verify 继续。
9
+ - 合并决定点也可接纳手修:要求新 HEAD 是当前候选后代且改动不越界;也可 `decide --action fix --repo <repo> --run <RUN> --reason <要修什么> --by <name>` 退回已配置的 fixer,再 `run --config <config> --run <RUN>`。两者都会重验、再审并重新等待合并决定,review 预算与历史证据保留。
10
+ - 修复后重新读取状态卡,复制带 `--candidate <完整 SHA>` 的批准命令;旧候选或未绑定候选的批准会报 `approval stale`。
7
11
  - 只有同机且持有者进程已不存在的锁自动回收。活动、异机、缺持有者信息的锁保留;stop 不是杀进程命令。
8
12
  - 超时、崩溃、无效信封、exit 75 属于 infra,不派 fixer、不扣失败预算。环境恢复后按待决策对象处理并续跑。
9
13
  - 台账损坏只读取合法前缀,拒绝追加;history --verify 校验回放,不会静默修复。保留现场与候选后决定恢复方式。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haiyangbg/buildbeat",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
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
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { execFileSync, spawn, spawnSync } from "node:child_process";
13
13
  import { createHash } from "node:crypto";
14
- import { existsSync, readdirSync, readFileSync } from "node:fs";
14
+ import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
15
15
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
 
@@ -32,6 +32,7 @@ import {
32
32
  approveRun,
33
33
  listInbox,
34
34
  rejectRun,
35
+ requestFix,
35
36
  } from "../runtime/decisions.js";
36
37
  import { checkRequires } from "../runtime/env-contract.js";
37
38
  import {
@@ -50,7 +51,7 @@ import {
50
51
  renderOverview,
51
52
  runsFor,
52
53
  } from "../runtime/overview.js";
53
- import { pathLabel, shellArg } from "../runtime/overview-repos.js";
54
+ import { configTargets, workConfigs, pathLabel, shellArg } from "../runtime/overview-repos.js";
54
55
  import { closeWork, runReadback } from "../runtime/release.js";
55
56
  import {
56
57
  DEFAULT_STALL_AFTER_MS,
@@ -97,7 +98,8 @@ Usage:
97
98
  buildbeat accept --repo <path> --work <WORK-ID> [--by <name>]
98
99
  buildbeat run --config <run-config.yaml> [--run <RUN-ID>] [--new] [--adopt <sha> --by <name>]
99
100
  buildbeat status --repo <path> [--work <WORK-ID> | --run <RUN-ID>] [--all-repos] [--json]
100
- buildbeat decide --repo <path> --run <RUN-ID> --action approve|reject --transition <t> [--by <name>] [--reason <text>]
101
+ buildbeat decide --repo <path> --run <RUN-ID> --action approve|reject --transition <t> [--candidate <sha>] [--by <name>] [--reason <text>]
102
+ buildbeat decide --repo <path> --run <RUN-ID> --action fix --reason <text> [--by <name>]
101
103
  buildbeat decide --repo <path> --work <WORK-ID> --action accept|dismiss --fingerprint <fp> [--by <name>] [--note <text>]
102
104
  buildbeat release --config <run-config.yaml> [--ref <ref>] [--note <text>]
103
105
  buildbeat decide --repo <path> --work <WORK-ID> --action close --result <text> [--by <name>]
@@ -179,8 +181,8 @@ function repoLabelFor(repoRoot, typed) {
179
181
  return rel;
180
182
  }
181
183
 
182
- function printNextReply(repoLabel, state) {
183
- const lines = nextReply({ repoLabel, state });
184
+ function printNextReply(repoLabel, state, repoRoot) {
185
+ const lines = nextReply({ repoLabel, state, repoRoot });
184
186
  if (lines.length === 0) {
185
187
  return;
186
188
  }
@@ -289,7 +291,7 @@ function printState(state, ledger, view = {}) {
289
291
  console.log(` reason: ${reason}`);
290
292
  }
291
293
  if (view.repoLabel) {
292
- printNextReply(view.repoLabel, state);
294
+ printNextReply(view.repoLabel, state, view.repoRoot);
293
295
  }
294
296
  }
295
297
  if (state.terminal) {
@@ -590,7 +592,7 @@ async function notifyForState(repoRoot, repoLabel, state) {
590
592
  }
591
593
  const results = await dispatchNotification(
592
594
  config,
593
- buildNotification(kind, { repoLabel, state }),
595
+ buildNotification(kind, { repoLabel, state, repoRoot }),
594
596
  { repoRoot },
595
597
  );
596
598
  for (const row of results) {
@@ -915,6 +917,7 @@ async function commandResume(
915
917
  sha: flags.adopt,
916
918
  by: flags.by ?? "human",
917
919
  resumeAt,
920
+ allowedPaths: options.allowedPaths,
918
921
  });
919
922
  console.log(
920
923
  `adopted ${adopted.adopted} as candidate (${adopted.decisionRef}, answers ${adopted.transition}); resuming at ${adopted.resumeAt}`,
@@ -1009,6 +1012,7 @@ function commandInbox(flags, rows = null) {
1009
1012
  for (const line of nextReply({
1010
1013
  repoLabel: row.repo ? shellArg(pathLabel(process.cwd(), rowRepo)) : repoLabelFor(repoRoot, flags.repo),
1011
1014
  state: ledger.state,
1015
+ repoRoot: rowRepo,
1012
1016
  })) {
1013
1017
  console.log(` next: ${row.config ? line.replaceAll("--config <run-config.yaml>", `--config ${shellArg(pathLabel(process.cwd(), row.config))}`) : line}`);
1014
1018
  }
@@ -1033,6 +1037,7 @@ function commandApprove(flags) {
1033
1037
  const result = approveRun(resolve(flags.repo), flags.run, {
1034
1038
  by: flags.by ?? "human",
1035
1039
  transition: flags.transition,
1040
+ candidate: flags.candidate,
1036
1041
  policies,
1037
1042
  });
1038
1043
  if (!result.approved) {
@@ -1628,6 +1633,28 @@ function commandDecide(flags) {
1628
1633
  }
1629
1634
  if (flags.action === "approve") return commandApprove(flags);
1630
1635
  if (flags.action === "reject") return commandReject(flags);
1636
+ if (flags.action === "fix") {
1637
+ if (!flags.repo || !flags.run) throw new Error("fix requires --repo and --run");
1638
+ const repoRoot = resolve(flags.repo);
1639
+ const prior = EventLedger.open(ledgerPathFor(repoRoot, flags.run));
1640
+ let hasFixer;
1641
+ // Older 4.x ledgers did not freeze repair capabilities. Discover a
1642
+ // single matching configuration; never guess between several configs.
1643
+ if (prior.state.run?.deliveryChecks && !prior.state.run.repair) {
1644
+ const configs = configTargets(workConfigs(repoRoot, prior.state.run.work), []);
1645
+ const candidates = configs.filter((item) => item.repo === realpathSync(repoRoot)).map((item) =>
1646
+ loadRunConfig({ config: item.path }, "resume")).filter((options) =>
1647
+ inRunFamily(options.runId, flags.run));
1648
+ if (candidates.length !== 1) throw new Error("cannot identify the run configuration; use manual repair and run --adopt");
1649
+ assertRunConfiguration(prior.state, candidates[0]);
1650
+ hasFixer = Boolean(candidates[0].adapters.fixer);
1651
+ }
1652
+ const result = requestFix(repoRoot, flags.run, {
1653
+ by: flags.by ?? "human", reason: flags.reason, hasFixer,
1654
+ });
1655
+ console.log(`repair requested as ${result.decisionRef}; continue with: buildbeat run --config <run-config.yaml> --run ${flags.run}`);
1656
+ return;
1657
+ }
1631
1658
  if (["accept", "dismiss"].includes(flags.action) && flags.fingerprint) {
1632
1659
  if (!flags.repo || !flags.work)
1633
1660
  throw new Error("finding decisions require --repo and --work");
@@ -1641,7 +1668,7 @@ function commandDecide(flags) {
1641
1668
  return;
1642
1669
  }
1643
1670
  throw new Error(
1644
- "decide requires --action approve|reject, accept|dismiss with --work and --fingerprint, or close with --work and --result",
1671
+ "decide requires --action approve|reject|fix, accept|dismiss with --work and --fingerprint, or close with --work and --result",
1645
1672
  );
1646
1673
  }
1647
1674
 
@@ -48,7 +48,7 @@ export const EVIDENCE_STATUSES = ["passed", "failed", "unverified"];
48
48
 
49
49
  export const EVIDENCE_GRADES = ["L0", "L1", "L2", "L3", "L4"];
50
50
 
51
- export const DECISION_VALUES = ["approved", "rejected"];
51
+ export const DECISION_VALUES = ["approved", "rejected", "fix"];
52
52
 
53
53
  // observe v0 per docs/v2/RFC-0003-workflow-policy.md §8 (frozen shape):
54
54
  // bands are exactly three fixed levels; triage actions are a closed set.
@@ -71,6 +71,7 @@ export function applyEvent(state, event) {
71
71
  planDigest: data.planDigest ?? "UNVERIFIED",
72
72
  intentDigest: data.intentDigest ?? "UNVERIFIED",
73
73
  ...(data.deliveryChecks ? { deliveryChecks: data.deliveryChecks } : {}),
74
+ ...(data.repair ? { repair: data.repair } : {}),
74
75
  };
75
76
  break;
76
77
  }
@@ -221,7 +222,7 @@ export function applyEvent(state, event) {
221
222
  subject: data.subject,
222
223
  decisionRef: data.decisionRef,
223
224
  });
224
- if (data.decision === "approved") {
225
+ if (data.decision === "approved" || data.decision === "fix") {
225
226
  next.approvals.push({
226
227
  decisionRef: data.decisionRef,
227
228
  transition: data.transition,
@@ -14,6 +14,8 @@ import {
14
14
  readdirSync,
15
15
  } from "node:fs";
16
16
  import { join } from "node:path";
17
+ import { spawnSync } from "node:child_process";
18
+ import { adjudicateFinding, fingerprintFinding, recordReviewFindings } from "./findings.js";
17
19
 
18
20
  import {
19
21
  deliveryChecks,
@@ -23,6 +25,7 @@ import {
23
25
  import { EventLedger } from "../storage/event-ledger.js";
24
26
  import {
25
27
  acquireLock,
28
+ listChangedPaths,
26
29
  readback,
27
30
  releaseLock,
28
31
  } from "../workspace/workspace-manager.js";
@@ -105,7 +108,7 @@ function recordDecisionFile(repoRoot, work, line) {
105
108
  export function approveRun(
106
109
  repoRoot,
107
110
  runId,
108
- { by = "human", transition, ts, policies } = {},
111
+ { by = "human", transition, ts, policies, candidate } = {},
109
112
  ) {
110
113
  // Read, check and write under the run lock: a ledger read before the
111
114
  // lock may be stale by the time it is written to.
@@ -113,6 +116,12 @@ export function approveRun(
113
116
  try {
114
117
  const ledger = openWaiting(repoRoot, runId);
115
118
  const pending = ledger.state.pendingHuman;
119
+ const repaired = ledger.events.some((event) => event.type === "DECISION_RECORDED" &&
120
+ event.data.transition === "enter-wait-merge" && (event.data.adopted || event.data.decision === "fix"));
121
+ if ((candidate && candidate !== pending.subject.candidate) ||
122
+ (pending.kind === "final-decision" && repaired && !candidate)) {
123
+ throw new DecisionError("approval stale: read the current decision card and bind approval to its candidate");
124
+ }
116
125
  if (!transition) {
117
126
  throw new DecisionError(
118
127
  `an approval must name its transition explicitly (pending: ${pending.transition})`,
@@ -291,7 +300,7 @@ export function approveRun(
291
300
  export function adoptCandidate(
292
301
  repoRoot,
293
302
  runId,
294
- { sha, by = "human", resumeAt, ts } = {},
303
+ { sha, by = "human", resumeAt, ts, allowedPaths } = {},
295
304
  ) {
296
305
  acquireLock(repoRoot, runId);
297
306
  try {
@@ -307,10 +316,12 @@ export function adoptCandidate(
307
316
  }
308
317
  const ledger = openWaiting(repoRoot, runId);
309
318
  const pending = ledger.state.pendingHuman;
310
- if (pending.kind === "final-decision") {
311
- throw new DecisionError(
312
- "adopt is for a run waiting before fix/verify, not at the merge decision",
313
- );
319
+ const final = pending.kind === "final-decision" && pending.transition === "enter-wait-merge";
320
+ if (pending.kind === "final-decision" && (!final || !ledger.state.run.deliveryChecks)) {
321
+ throw new DecisionError("legacy active run: use its original 3.3.1 runtime (docs/MIGRATION.md)");
322
+ }
323
+ if (final && resumeAt !== "verify") {
324
+ throw new DecisionError("a final-decision repair must resume at verify");
314
325
  }
315
326
  const bound = ledger.state.workspaces[runId];
316
327
  const worktreePath = bound
@@ -332,6 +343,19 @@ export function adoptCandidate(
332
343
  `worktree HEAD is ${tree.head}, not ${sha}; adopt what git reads back`,
333
344
  );
334
345
  }
346
+ if (final) {
347
+ const candidate = bound.candidate ?? bound.base;
348
+ const ancestor = spawnSync("git", ["-C", worktreePath, "merge-base", "--is-ancestor", candidate, tree.head]);
349
+ if (tree.head === candidate || ancestor.status !== 0) {
350
+ throw new DecisionError("adopt requires a new descendant of the current candidate; history rewrites are refused");
351
+ }
352
+ }
353
+ const scope = ledger.state.run.repair?.allowedPaths ?? allowedPaths;
354
+ if (scope) {
355
+ const violations = listChangedPaths(worktreePath, bound.base).filter((path) =>
356
+ !scope.some((prefix) => path === prefix || path.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`)));
357
+ if (violations.length) throw new DecisionError(`out-of-scope changes: ${violations.join(", ")}`);
358
+ }
335
359
  const when = ts ?? new Date().toISOString();
336
360
  const lastEvidence =
337
361
  ledger.state.evidence[ledger.state.evidence.length - 1];
@@ -390,6 +414,51 @@ export function adoptCandidate(
390
414
  }
391
415
  }
392
416
 
417
+ // Queue a human-requested repair without fabricating a review or a budget
418
+ // grant. The ordinary resume loop consumes this decision at the fix step.
419
+ export function requestFix(repoRoot, runId, { by = "human", reason, ts, hasFixer } = {}) {
420
+ acquireLock(repoRoot, runId);
421
+ try {
422
+ const ledger = openWaiting(repoRoot, runId);
423
+ const pending = ledger.state.pendingHuman;
424
+ if (!ledger.state.run.deliveryChecks)
425
+ throw new DecisionError("legacy active run: use its original 3.3.1 runtime (docs/MIGRATION.md)");
426
+ if (pending.kind !== "final-decision" || pending.transition !== "enter-wait-merge")
427
+ throw new DecisionError("fix is only available at the merge decision");
428
+ if (!(ledger.state.run.repair?.hasFixer ?? hasFixer))
429
+ throw new DecisionError("no fixer configured; commit a manual repair and use run --adopt <sha> --by <name>");
430
+ if (typeof reason !== "string" || !reason.trim())
431
+ throw new DecisionError("fix requires a non-empty reason describing what to repair");
432
+ const bound = ledger.state.workspaces[runId];
433
+ const worktreePath = bound && resolveRepoRef(repoRoot, bound.worktreePath);
434
+ if (!worktreePath || !existsSync(worktreePath))
435
+ throw new DecisionError(`worktree missing for ${runId}`);
436
+ const tree = readback(worktreePath);
437
+ if (tree.dirty || tree.head !== (bound.candidate ?? bound.base) || tree.head !== pending.subject.candidate)
438
+ throw new DecisionError("worktree changed since the request; commit the manual repair and use run --adopt");
439
+ const when = ts ?? new Date().toISOString();
440
+ const decisionRef = `D-${runId}-${ledger.state.decisions.length + 1}`;
441
+ const finding = { severity: "P1", summary: reason.trim() };
442
+ recordReviewFindings(repoRoot, ledger.state.run.work, {
443
+ run: runId, step: "enter-wait-merge", attempt: ledger.state.decisions.length + 1,
444
+ findings: [finding], ts: when,
445
+ });
446
+ adjudicateFinding(repoRoot, ledger.state.run.work, {
447
+ fingerprint: fingerprintFinding(finding), action: "accept", by,
448
+ note: decisionRef, ts: when,
449
+ });
450
+ const data = {
451
+ decision: "fix", transition: pending.transition, subject: pending.subject,
452
+ decisionRef, reason: finding.summary, resumeAt: "fix",
453
+ };
454
+ ledger.append({ type: "DECISION_RECORDED", actor: { kind: "human", id: by }, ts: when, data });
455
+ recordDecisionFile(repoRoot, ledger.state.run.work, { ts: when, run: runId, ...data, by });
456
+ return { decisionRef, state: ledger.state };
457
+ } finally {
458
+ releaseLock(repoRoot, runId);
459
+ }
460
+ }
461
+
393
462
  // Accepts a work artifact (plan, intent, spec) by binding a decision to the
394
463
  // file's current digest in the Git plane. If the file changes afterwards,
395
464
  // artifact.accepted evaluates false again — acceptance cannot go stale
@@ -10,9 +10,10 @@
10
10
 
11
11
  import { payloadFor } from "../adapters/notification-payload.js";
12
12
  import { currentScreenshots } from "../policy/policy.js";
13
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
13
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
 
16
+ import { configTargets, workConfigs } from "./overview-repos.js";
16
17
  import { parseYamlSubset } from "../engine/yaml-subset.js";
17
18
 
18
19
  export const NOTIFY_CONFIG = join(".buildbeat", "notify.yaml");
@@ -89,7 +90,22 @@ export function candidateScreenshots(state, candidate) {
89
90
 
90
91
  // The exact commands a human can copy to answer a pending request. Shared by
91
92
  // status, inbox and notifications so "what do I say now" has one answer.
92
- export function nextReply({ repoLabel, state }) {
93
+ function configuredFixer(repoRoot, state) {
94
+ if (state.run.repair) return state.run.repair.hasFixer;
95
+ if (!repoRoot) return false;
96
+ // Existing 4.x runs have no repair snapshot. Hints are conservative when
97
+ // their configuration is missing, ambiguous or unreadable.
98
+ try {
99
+ const configs = configTargets(workConfigs(repoRoot, state.run.work), [])
100
+ .filter((item) => item.repo === realpathSync(repoRoot))
101
+ .map((item) => parseYamlSubset(readFileSync(item.path, "utf8")))
102
+ .filter((doc) => doc.run === state.run.id || (typeof doc.run === "string" &&
103
+ state.run.id.startsWith(`${doc.run}-`) && /^\d+$/.test(state.run.id.slice(doc.run.length + 1))));
104
+ return configs.length === 1 && Boolean(configs[0].workers?.fixer?.command);
105
+ } catch { return false; }
106
+ }
107
+
108
+ export function nextReply({ repoLabel, state, repoRoot }) {
93
109
  const pending = state.pendingHuman;
94
110
  if (!pending || !state.run) {
95
111
  return [];
@@ -120,14 +136,21 @@ export function nextReply({ repoLabel, state }) {
120
136
  );
121
137
  }
122
138
  lines.push(
123
- `buildbeat decide --action approve --repo ${repoLabel} --run ${runId} --transition ${pending.transition} --by <you>` +
139
+ `buildbeat decide --action approve --repo ${repoLabel} --run ${runId} --transition ${pending.transition}` +
140
+ (pending.kind === "final-decision" ? ` --candidate ${pending.subject.candidate}` : "") + " --by <you>" +
124
141
  (pending.kind === "final-decision" ? " # merge-ready; merge/push stay yours" : " # then: run --config <run-config.yaml>"),
125
142
  );
126
143
  lines.push(`buildbeat decide --action reject --repo ${repoLabel} --run ${runId} --reason <why> --by <you>`);
144
+ if (pending.kind === "final-decision") {
145
+ lines.push(`buildbeat run --config <run-config.yaml> --run ${runId} --adopt <sha> --by <you> # commit a manual repair in the run worktree; verify and review again`);
146
+ if (configuredFixer(repoRoot, state)) {
147
+ lines.push(`buildbeat decide --repo ${repoLabel} --run ${runId} --action fix --reason <what to repair> --by <you> # then: run --config <run-config.yaml> --run ${runId}`);
148
+ }
149
+ }
127
150
  return lines;
128
151
  }
129
152
 
130
- export function buildNotification(kind, { repoLabel, state, detail = {} }) {
153
+ export function buildNotification(kind, { repoLabel, state, repoRoot, detail = {} }) {
131
154
  const run = state.run;
132
155
  const base = {
133
156
  kind,
@@ -149,7 +172,7 @@ export function buildNotification(kind, { repoLabel, state, detail = {} }) {
149
172
  reasons: pending?.reasons ?? [],
150
173
  candidate: pending?.subject?.candidate ?? null,
151
174
  ...(screenshots.length > 0 ? { screenshots } : {}),
152
- nextReply: nextReply({ repoLabel, state }),
175
+ nextReply: nextReply({ repoLabel, state, repoRoot }),
153
176
  };
154
177
  }
155
178
  if (kind === "RUN_TERMINAL") {
@@ -744,6 +744,21 @@ function beginStep(context, step, stepDef, attempt) {
744
744
  }));
745
745
  }
746
746
  }
747
+ // A human's final-decision repair is separate from reviewer evidence: it
748
+ // must not spend a review round or pretend that a reviewer reported it.
749
+ if (step === "fix") {
750
+ const decision = [...ledger.events].reverse().find((event) =>
751
+ event.type === "DECISION_RECORDED" && event.data.decision === "fix");
752
+ const priorFix = [...ledger.events].reverse().find((event) =>
753
+ event.type === "STEP_FINISHED" && event.data.step === "fix" && event.data.status === "succeeded");
754
+ if (decision && (!priorFix || decision.seq > priorFix.seq)) {
755
+ const finding = { severity: "P1", summary: decision.data.reason };
756
+ input.findings = [...(input.findings ?? []).filter((item) =>
757
+ item.fingerprint !== fingerprintFinding(finding)), {
758
+ ...finding, fingerprint: fingerprintFinding(finding), adjudication: "accept",
759
+ }];
760
+ }
761
+ }
747
762
  // Envelope (C6): the worker's prompt, materialised into the run
748
763
  // directory and handed over as BUILDBEAT_PROMPT / input.envelope.
749
764
  const prompt = materialisePrompt({
@@ -1506,6 +1521,9 @@ export function startRun(options) {
1506
1521
  run: runId,
1507
1522
  work: workId,
1508
1523
  data: {
1524
+ ...(options.deliveryChecks ? {
1525
+ repair: { hasFixer: Boolean(options.adapters?.fixer), allowedPaths: options.allowedPaths ?? null },
1526
+ } : {}),
1509
1527
  workflowRef: workflow.name,
1510
1528
  workflowDigest,
1511
1529
  base: workspace.base,
@@ -1574,6 +1592,10 @@ export function assertRunConfiguration(state, options) {
1574
1592
  if (!state.run) throw new OrchestratorError("run has no creation record");
1575
1593
  if (options.workId && options.workId !== state.run.work)
1576
1594
  throw new OrchestratorError("run belongs to a different work");
1595
+ if (state.run.repair && (
1596
+ canonicalJson(state.run.repair.allowedPaths) !== canonicalJson(options.allowedPaths ?? null) ||
1597
+ state.run.repair.hasFixer !== Boolean(options.adapters?.fixer)
1598
+ )) throw new OrchestratorError("repair configuration changed since the run was created; refusing to resume");
1577
1599
  const { workflowDigest } = options;
1578
1600
  if (
1579
1601
  state.run.deliveryChecks &&
@@ -324,7 +324,7 @@ export function computeOverview(
324
324
  ? "MERGE_DECISION"
325
325
  : "WAITING_HUMAN";
326
326
  const replies = latest.state
327
- ? nextReply({ repoLabel, state: latest.state })
327
+ ? nextReply({ repoLabel, state: latest.state, repoRoot })
328
328
  : [];
329
329
  next = replies[0] ?? `buildbeat status --repo ${repoLabel} --work ${workId}`;
330
330
  } else if (latest.status === "SUCCEEDED" && isReleaseLane(latest)) {