@haiyangbg/buildbeat 3.0.0 → 3.1.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.en.md +4 -4
  3. package/README.md +1 -1
  4. package/SKILL.md +8 -5
  5. package/docs/README.md +3 -2
  6. package/docs/RELEASING.md +3 -1
  7. package/docs/v2/guide/01-quickstart.en.md +165 -0
  8. package/docs/v2/guide/01-quickstart.md +7 -3
  9. package/docs/v2/guide/02-workflow-guide.md +11 -0
  10. package/docs/v2/guide/06-evidence-guide.en.md +51 -0
  11. package/docs/v2/guide/06-evidence-guide.md +2 -0
  12. package/docs/v2/guide/07-approval-guide.en.md +117 -0
  13. package/docs/v2/guide/07-approval-guide.md +16 -2
  14. package/docs/v2/guide/10-recovery.en.md +99 -0
  15. package/docs/v2/guide/10-recovery.md +22 -4
  16. package/docs/v2/guide/README.md +4 -4
  17. package/example/.buildbeat/notify.yaml +13 -0
  18. package/example/.buildbeat/observe.yaml +31 -0
  19. package/example/AGENTS.md +73 -0
  20. package/example/BUILDBEAT.md +14 -0
  21. package/example/CLAUDE.md +7 -0
  22. package/example/README.md +25 -0
  23. package/example/delivery/envelope/prompts/builder.md +10 -0
  24. package/example/delivery/envelope/prompts/fixer.md +10 -0
  25. package/example/delivery/envelope/prompts/reviewer.md +13 -0
  26. package/example/delivery/envelope/worker.sh +70 -0
  27. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/decisions.jsonl +3 -0
  28. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/intent.md +24 -0
  29. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/plan.md +20 -0
  30. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/run-config.yaml +66 -0
  31. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/runs/RUN-EXPORT-01/run-record.json +108 -0
  32. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/workflow.yaml +44 -0
  33. package/example/gitignore.template +20 -0
  34. package/example/package.json +13 -0
  35. package/example/pm/decisions.md +8 -0
  36. package/example/src/export.js +25 -0
  37. package/example/src/ledger.js +16 -0
  38. package/example/tests/export.test.js +35 -0
  39. package/example//346/214/207/346/214/245/345/217/260.md +40 -0
  40. package/package.json +2 -1
  41. package/src/v2/cli/run-config-check.js +223 -0
  42. package/src/v2/cli/run.js +74 -14
  43. package/src/v2/engine/reducer.js +6 -0
  44. package/src/v2/engine/yaml-subset.js +44 -9
  45. package/src/v2/runtime/decisions.js +32 -30
  46. package/src/v2/runtime/gc.js +28 -3
  47. package/src/v2/runtime/orchestrator.js +223 -96
  48. package/src/v2/storage/event-ledger.js +22 -3
  49. package/src/v2/workspace/workspace-manager.js +182 -13
  50. package/templates/v2/run-config.example.yaml +3 -1
@@ -0,0 +1,117 @@
1
+ # Human approval guide
2
+
3
+ [简体中文](07-approval-guide.md) | **English**
4
+
5
+ Authority: [`RFC-0003 §5`](../RFC-0003-workflow-policy.md) (Chinese); implementation: `src/v2/runtime/decisions.js`. Principle: **a human approves a digest-bound subject, not a sentence that says "fine"**.
6
+
7
+ ## What an approval binds
8
+
9
+ One approval = the quadruple `transition + candidate + planDigest + evidenceDigest`. If any of the four changes afterwards, the approval becomes `APPROVAL_STALE` on its own and the Run returns to `WAITING_HUMAN`: an old stamp never covers a new subject (zero reuse of stale approvals is an exit metric; the pilots measured zero).
10
+
11
+ ## Daily commands
12
+
13
+ ```bash
14
+ buildbeat inbox --repo . # every Run waiting for a human: transition, candidate, digest, reason
15
+ buildbeat status --repo . --run RUN-X # the full derived view of one Run (steps, evidence, findings)
16
+ buildbeat approve --repo . --run RUN-X --transition enter-wait-merge --by <name> --config <run-config>
17
+ buildbeat reject --repo . --run RUN-X --reason "<why>" --by <name>
18
+ buildbeat accept --repo . --work WORK-X --artifact plan --by <name> # artifact acceptance (digest-bound)
19
+ ```
20
+
21
+ Decisions land in the Git plane at `delivery/work/<id>/decisions.jsonl`; the event ledger records `DECISION_RECORDED` at the same time.
22
+
23
+ ## The safety semantics of approve (all tested)
24
+
25
+ 1. **The transition must match** the pending request;
26
+ 2. **Reality is re-read before stamping**: if the pending snapshot and reality disagree (the candidate moved, the plan changed), the approval is refused and a refresh is required; nothing is stamped;
27
+ 3. **Transition gates are re-checked at the instant of stamping**: if merge-evidence-floor / ui-render-gate do not PASS right then, the approval is refused;
28
+ 4. Approving a final decision (a pending request of the final-decision kind) means `RUN_TERMINAL SUCCEEDED` plus compacting the run-record into the Git plane.
29
+
30
+ ## Five words and what each means (approving is not executing)
31
+
32
+ Sessions and documents used to mix "accept / approve / resume / succeeded / merged". The vocabulary is fixed as follows:
33
+
34
+ | Word | Command | Meaning | Is not |
35
+ |---|---|---|---|
36
+ | **Accept** | `accept --artifact intent\|plan` | A human endorses one artifact's digest; editing it makes the 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; 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
+ | **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
+ | **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
+ | **Reject** | `reject --reason` | The Run ends (`FAILED`, reason recorded) | The artifacts are invalidated; the acceptance state of intent/plan does not change |
41
+
42
+ Likewise, `fix_now` on an observe draft is only acceptance; a human starts the Run. Protected actions are listed under [Security boundaries](09-security-boundaries.md) (Chinese).
43
+
44
+ After `start --attempt new` numbers a Run, `resume --config <run-config.yaml>` resumes the family’s only non-terminal Run and prints its ID. Use `--run <RUN-ID>` to select the configured Run itself or `<family>-NN` explicitly (at least two digits). An existing ledger for the exact configured ID takes precedence. Multiple non-terminal Runs are listed with a request to select one using `--run`; if none remain, the error reports the latest ID and terminal status, or states that no ledgers were found.
45
+
46
+ ## Risk presets decide where humans approve
47
+
48
+ `fast`: Merge only; `standard`: Plan + Merge; `controlled`: Intent + Plan + Merge + Release. A pending request must carry the findings summary and the reason, which prevents the "rubber stamp" decay; human waiting time goes into `metrics`.
49
+
50
+ ## The finding triage gate and anchored review
51
+
52
+ > Since 2.0.0-beta.3.
53
+ The biggest structural lesson of the thirty-round deployment campaign: **a finding is a prescription, not a fact**. A memoryless fresh reviewer writes contradictory prescriptions and overturns designs that were accepted long ago; routing a fixer automatically turns that oscillation straight into cost. Two mechanisms work together:
54
+
55
+ 1. **The triage gate**: with `reviewTriage: required` in the run config, P0/P1 findings from review no longer dispatch a fixer automatically; the Run stops at `WAITING_HUMAN` (kind `finding-triage`) and the pending reason lists every finding fingerprint. A human adjudicates first, then `approve --transition enter-fix` releases the fixer (or `reject` ends the Run).
56
+ 2. **The adjudication ledger**: every finding lands in the Git plane at `delivery/work/<id>/review-findings.jsonl` (fingerprint = hash of severity plus normalized text):
57
+
58
+ ```bash
59
+ buildbeat findings list --repo . --work WORK-X
60
+ buildbeat findings adjudicate --repo . --work WORK-X --fingerprint <fp> --action dismiss --by <name> --note "<why>"
61
+ ```
62
+
63
+ After `dismiss`, the same fingerprint no longer blocks (raising it again is recorded visibly as `RE-RAISED`, but does not restart the loop); **a severity upgrade is a new fingerprint and blocks again on its own**: noise is suppressed, real signal is not, the same principle as observe's dismiss feedback.
64
+ 3. **Anchor injection**: the reviewer (a readonly step) receives `anchor` in `BUILDBEAT_INPUT`, the full table of past findings and adjudications, and the envelope prompt should tell the reviewer that adjudicated conclusions must not be overturned; writing steps such as the fixer receive `findings` (last round's findings with their adjudication state), and the fixer repairs only accepted/open ones instead of guessing.
65
+
66
+ Adjudication memory lives in the Git plane; deleting the runtime does not lose it (covered by the same tests as invariant 23).
67
+
68
+ ## Waiting must be able to find a person
69
+
70
+ > Since 2.0.0-beta.4 (iteration 08).
71
+ In the pilot workspace, 32 of 58 Runs were cancelled, most of them after hanging in `WAITING_HUMAN` for a full day; the average human wait was 7 to 12 hours. The cause was not slow people but **nobody knowing something was waiting for them**. Three things go together:
72
+
73
+ 1. **What to say next**: `status` and `inbox` print the copyable command right after each wait (`approve` / `reject`, plus `findings list|adjudicate` during triage); `inbox` groups by Work and shows how long each has waited. The `--repo` in that output is a relative path inside the project and the placeholder `<repo-path>` outside it: a machine-local absolute path never enters the output.
74
+ 2. **A new Run of the same Work supersedes the old wait**: on `start`, older Runs of the same Work that are still waiting are recorded as `SUPERSEDED` (terminal, compacted into a run-record), and the new Run's `RUN_CREATED.data.supersedes` records the lineage; the inbox keeps only live waits. Write `supersede: off` in the run config if you do not want this. RUNNING Runs are unaffected (active lock); old Runs locked by another process are skipped and reported.
75
+ 3. **Outbound notifications**: `.buildbeat/notify.yaml` in the Git plane:
76
+
77
+ ```yaml
78
+ kind: notify
79
+ version: 1
80
+ channels:
81
+ - id: owner
82
+ type: dingtalk # or webhook
83
+ urlEnv: BUILDBEAT_NOTIFY_URL # the URL comes only from an environment variable; a literal url is refused
84
+ events:
85
+ - HUMAN_REQUESTED
86
+ - RUN_TERMINAL
87
+ - STALLED
88
+ ```
89
+
90
+ The CLI sends when a Run stops for a human or reaches a terminal state; subscribing to `STALLED` makes `start`/`resume` spawn a detached `watch` process that watches for silent worker output (threshold `stallAfterMs`, default 15 minutes). A failed send is only logged to `runs/<RUN>/notify.log` and the screen and **never affects the Run**; the payload carries identifiers, the reason, the candidate SHA and the next command, with zero logs and zero candidate content. A DingTalk custom robot needs its keyword configured (default `BuildBeat`). `doctor` reports whether the channel and its environment variable are in place.
91
+
92
+ Notification is not an approval channel: decisions are still made only through the CLI, with digest binding unchanged.
93
+
94
+ ## From "waiting for me" to "where are we": overview
95
+
96
+ > Since 2.0.0-beta.4 (iteration 08).
97
+ `inbox` only knows which Run waits for a human; `buildbeat overview --repo .` answers per Work "how far, whose move next": whether intent/plan are accepted (edited after acceptance means `stale`), the latest Run's state and candidate, whether the candidate is merged into the current branch, the number of unadjudicated P0/P1 findings, whether `env-facts.md` exists, each row with its next command. After the runtime is deleted, the Git-plane run-records fill in. A session runs it first, then answers "where are we".
98
+
99
+ **Stage truth corrections (iteration 09)**: once a candidate is merged into the current branch the Work is `MERGED`, even if the latest Run is CANCELLED (a pilot login Run was cancelled over a budget problem while its candidate was already in production, and overview reported `STOPPED_CANCELLED` and urged a retry); a Work whose `release-readback` lane closed successfully shows `RELEASED` instead of "nothing to merge"; merged / released / closed Works no longer report unadjudicated finding counts. Each Work in `overview` also carries a `cost:` line (see the Work-level budgets in the [Workflow guide](02-workflow-guide.md), Chinese).
100
+
101
+ ## You fixed it yourself: `resume --adopt`
102
+
103
+ > Since 2.0.0-beta.5 (iteration 09).
104
+ When a Run stops at `enter-fix` / `resume-fix`, the driving session or a person has often already fixed the problem in the Run's worktree and committed it. Approving at that point dispatches a fixer with nothing to do and runs verify once more (a pilot frontend Run reached its 5th verify and 3rd fix this way). Use instead:
105
+
106
+ ```bash
107
+ buildbeat resume --config <run-config.yaml> --run <RUN-ID> --adopt <sha> --by <name>
108
+ ```
109
+
110
+ The kernel reads the worktree back: the tree must be clean and HEAD must be exactly `<sha>` (7-character prefix or longer), otherwise it refuses; then it records `CANDIDATE_PINNED` with a human actor (`adopted: true`), records `DECISION_RECORDED` with that commit as subject (`adopted`, `resumeAt`), and continues from verify (the step after a successful fix in the preset). The ledger shows who supplied this candidate. Adoption is not accepted at the merge decision.
111
+
112
+ `doctor` now also prints whether intent / plan exist and are accepted in this repository's `delivery/work/<ID>/`, and for every policy requiring `artifact.accepted` it previews where `start` will stop; before that, doctor passed twice while start was blocked by "plan not mirrored into the sub-repository".
113
+
114
+ ## Visible names are gate decisions
115
+
116
+ > Since 2.0.0-beta.4 (iteration 08).
117
+ The `BATCH_AT_GATE` tier of the three approval tiers explicitly includes domain names, service names, environment names, auto-stop durations, window durations: **names and parameters the owner will later see or say out loud**. Names a worker picks in passing never reach the ledger; the planner lists them in the intent with recommended values, and the human approves them in one go.
@@ -1,5 +1,7 @@
1
1
  # Human Approval 指南
2
2
 
3
+ **简体中文** | [English](07-approval-guide.en.md)
4
+
3
5
  权威:[`RFC-0003 §5`](../RFC-0003-workflow-policy.md);实现:`src/v2/runtime/decisions.js`。原则:**人批的是一个 digest 绑定的对象,不是一句"可以了"**。
4
6
 
5
7
  ## 批准绑定什么
@@ -32,17 +34,29 @@ buildbeat accept --repo . --work WORK-X --artifact plan --by <名字> # 工
32
34
  | 词 | 命令 | 含义 | 不等于 |
33
35
  |---|---|---|---|
34
36
  | **接受**(accept) | `accept --artifact intent\|plan` | 一份工件的 digest 被人认可;改过即 `stale` | 开工;不产生任何 Run |
35
- | **批准某转换**(approve) | `approve --transition <t>` | 允许 Run 走**这一条** transition:`enter-fix`(放行分诊后的 fixer)、`resume-<step>`(预算耗尽 / infra 停人后再跑一次,会落 `BUDGET_EXTENDED`)、`enter-review`(Work 级 review 上限后再审一轮)、`enter-apply-readback`(上线车道"我做完了") | 批准了别的转换;非终态转换批准后 Run **不会自己动**,要 `resume --config <run-config>` 续跑(`approve` 输出的 `next:` 行会写明) |
37
+ | **批准某转换**(approve) | `approve --transition <t>` | 允许 Run 走**这一条** transition:`enter-fix`(放行 fixer;附带 grants 时还放行重验后的下一轮 review)、`resume-<step>`(预算耗尽后扩额,或 infra 退款后重试)、`enter-review`(Work 级 review 上限后再审一轮)、`enter-apply-readback`(上线车道"我做完了") | 批准了别的转换;非终态转换批准后 Run **不会自己动**,要 `resume --config <run-config>` 续跑(`approve` 输出的 `next:` 行会写明) |
36
38
  | **合并决定**(最终批准) | `approve --transition enter-wait-merge` | 候选已具备合并条件:candidate + planDigest + evidenceDigest 此刻全部成立;Run 进终态 `SUCCEEDED`,run-record 压进 Git 面 | 代码已合并、已 push、已部署——这三件永远是你在 Runner 之外的动作 |
37
39
  | **Run SUCCEEDED** | — | Run 停在了它该停的地方,证据齐 | Work 完成。`overview` 只有回读到候选在当前分支上才显示 `MERGED` |
38
40
  | **拒绝**(reject) | `reject --reason` | Run 终止(`FAILED`,理由入账) | 工件失效;intent/plan 的接受状态不变 |
39
41
 
40
42
  同理 observe 草稿的 `fix_now` 只是接受,Run 由人发起。保护动作见 [安全边界](09-security-boundaries.md)。
41
43
 
44
+ 使用 `start --attempt new` 自动编号时,`resume --config <run-config.yaml>` 会续跑该家族唯一未终态的 Run,并打印选中的 ID;也可用 `--run <RUN-ID>` 显式指定配置中的 Run 本身或 `<家族>-NN`(数字至少两位)。配置本身已有台账时优先使用该精确 ID。多个未终态 Run 会列出候选并要求用 `--run` 选择;没有未终态 Run 会报告最新一次的 ID 和终态,没有台账则明确说明。
45
+
42
46
  ## 人批点由 Risk Preset 决定
43
47
 
44
48
  `fast` 仅 Merge;`standard` Plan+Merge;`controlled` Intent+Plan+Merge+Release。待批项强制携带 findings 摘要与理由——防"秒批"退化;人批等待时长进 `metrics`。
45
49
 
50
+ ## 预算停车:成功不扣次数,一轮一问
51
+
52
+ - 非只读步(build / verify / fix)成功的 attempt 不扣 `maxAttempts`,只读步仍消耗次数,review 按轮计费。`STEP_FINISHED.free: true` 与 `BUDGET_CONSUMED.amount: 0` 记录退款;没有新字段的旧台账保持原有回放结果。
53
+ - 真失败到顶仍停 `resume-<step>`,同指纹两次仍停,release 预设的 `maxAttempts: 1` 仍有效。infra 故障仍不扣次数。默认预算数值不变。
54
+ - review 发现阻断问题时,若下一轮会超过 Run 或 Work 上限,立即停 `enter-fix`,在花费修复、重验之前问一次。有分诊时 kind 为 `finding-triage`,无分诊时为 `budget`。批准表示「修复 + 重新验证 + 再审一轮」;拒绝结束本 Run,由人按现有证据决定是否合并。
55
+ - 每一次预算停车(`enter-fix`、`resume-<step>`、Work 级 `enter-review`)都在请求上记可选 `grants`,列出下一次执行会撞到的 Run/Work 上限;批准一次即同时放行两层,不再连问两次。`resume` 校验批准仍有效后逐条落 `BUDGET_EXTENDED`,并把整份放行计划钉在第一条上:放行落到一半进程被杀,再次 `resume` 按钉住的计划补齐剩余项,不从已被抬高的状态重算。过期批准、新请求不继承旧 grants。会话在 worktree 里手修并用 `resume --adopt <sha>` 回答该请求时,候选虽换成新提交,仍继承请求上的 grants(grants 属于这一轮,不属于某个候选;计划变了则不继承),修完重验后直接进入下一轮 review,不再二次停车。
56
+ - 防止自定义 workflow 的成功循环失控:同一步总 attempt 达到有效上限(配置预算 + 人批扩额)的 **3 倍**后,在下一次执行前仍以 kind `budget` 兜底停人。成功/infra 退款不增加兜底上限;批准扩额会提高它。
57
+
58
+ 停车首行显示已用次数(或 review 轮数)、有效上限与真失败次数,并保留 `budget` 标识供度量使用。`status` 的 attempt 序号和 `overview` 的成本累计仍表示实际运行量,不是失败次数;`doctor` 展示配置上限。
59
+
46
60
  ## 发现分诊门与锚定审查
47
61
 
48
62
  > 自 2.0.0-beta.3(beta.3)起。
@@ -100,7 +114,7 @@ buildbeat accept --repo . --work WORK-X --artifact plan --by <名字> # 工
100
114
  Run 停在 `enter-fix` / `resume-fix` 时,驾驶会话或人常常已经在 Run 的 worktree 里把问题修掉并提交了。此时再 `approve` 会派一个无事可做的 fixer,再多跑一次 verify(试点一条前端 Run 因此跑到 verify 第 5 次、fix 第 3 次)。改用:
101
115
 
102
116
  ```bash
103
- buildbeat resume --config <run-config.yaml> --adopt <sha> --by <名字>
117
+ buildbeat resume --config <run-config.yaml> --run <RUN-ID> --adopt <sha> --by <名字>
104
118
  ```
105
119
 
106
120
  内核回读 worktree:树必须干净、HEAD 必须就是 `<sha>`(前缀 7 位起),否则拒绝;然后以人为 actor 落 `CANDIDATE_PINNED`(`adopted: true`)、以该提交为 subject 记 `DECISION_RECORDED`(`adopted`、`resumeAt`),并从 verify 继续(预设里 fix 成功后的下一步)。台账里看得出这一版候选是谁供的。合并决定处不接受 adopt。
@@ -0,0 +1,99 @@
1
+ # Recovery handbook
2
+
3
+ [简体中文](10-recovery.md) | **English**
4
+
5
+ Design premise (invariant 23 in [`V2-PLAN.md`](../../V2-PLAN.md), Chinese): **the whole `.buildbeat/runtime/` directory can be deleted at any time**. Accepted artifacts, decisions, Intent drafts with their triage, and the compacted records of finished Runs all live in the Git plane. "Delete and rebuild" is the default troubleshooting move, not the last resort.
6
+
7
+ ## Symptom → action
8
+
9
+ ### "run config … has N problem(s)"
10
+
11
+ The run config has mistakes; no Run started and nothing changed. Fix the list item by item (each names the key, what is wrong and the closest valid spelling), then rerun the same command; `buildbeat doctor --config …` checks it on its own first.
12
+
13
+ ### The ledger reports corrupted
14
+
15
+ `status`/`inbox` shows `LEDGER CORRUPTED after seq=N (<reason>)`: the ledger truncates its view at the last valid event and **refuses to append**; recovery is a human decision, nothing is repaired silently.
16
+
17
+ 1. `buildbeat events --repo . --run RUN-X` shows the valid prefix; `replay` verifies the reduction;
18
+ 2. If the broken Run is in flight: usually abandon it (the candidate in the worktree is still readable on its branch) and start a new Run;
19
+ 3. If someone edited the ledger file by hand: rebuild the judgement from Git-plane facts; do not patch event lines by hand.
20
+
21
+ ### The Run process was killed / the machine rebooted
22
+
23
+ ```bash
24
+ buildbeat resume --config <run-config.yaml>
25
+ ```
26
+
27
+ After `start --attempt new` numbers a Run, `resume --config <run-config.yaml>` resumes the family’s only non-terminal Run and prints its ID. Use `--run <RUN-ID>` to select the configured Run itself or `<family>-NN` explicitly (at least two digits). An existing ledger for the exact configured ID takes precedence. Multiple non-terminal Runs are listed with a request to select one using `--run`; if none remain, the error reports the latest ID and terminal status, or states that no ledgers were found.
28
+
29
+ The in-flight step is closed as `crashed` (the fact is recorded), then **the step itself is rerun** (changed in beta.3): a dead process says nothing about the candidate; the lost attempt still counts against the step's budget, and an exhausted budget stops for a human. The earlier semantics treated a crash as a step failure and followed the failure edge; real incident (deploy-18): the host tool's timeout killed the verify worker, the crash was routed to fix, and the fixer burned a round facing zero verifier evidence. A dirty worktree still stops for a human first. Resuming with approvals re-checks candidate/plan freshness and turns `APPROVAL_STALE` over to a human if anything changed. If it cannot be recovered, delete the runtime and rerun: the candidate branch and the Git-plane records are not lost.
30
+
31
+ **Launch discipline** (the other half of the same incident): a Run longer than minutes must be launched in a way that escapes the host tool's timeout (`nohup`/`setsid`); `start` prints this reminder in an interactive shell.
32
+
33
+ ### A stuck lock ("another run is active")
34
+
35
+ Every lock records its owner (pid, host name, time acquired, command). When a driver is killed, ended by a host-tool timeout or lost to a reboot, its locks stay behind; the next `resume` / `stop` / `start` that needs the lock reclaims it automatically when the owner is **on this host and its process no longer exists**, printing `reclaimed stale lock <id> (owner pid … is gone)`. No file needs deleting by hand. After a killed driver, simply run:
36
+
37
+ ```bash
38
+ buildbeat resume --config <run-config.yaml>
39
+ ```
40
+
41
+ The kernel takes the crash-recovery path above (`RUN_INTERRUPTED`, then the interrupted step reruns); if you would rather not continue, `buildbeat stop --repo . --run RUN-X --reason "…"`.
42
+
43
+ Only three cases still answer `another run is active` / `already locked`, and the error names the owner:
44
+
45
+ - **The owner is alive**: another Run really is driving; wait for it, or end that process once you are sure it is stuck;
46
+ - **The owner is on another host** (a repository on a shared disk): deal with it on that machine; this host never reclaims it;
47
+ - **No owner record**: a lock left by an older buildbeat, or a crash in the instant of taking it; once no buildbeat process is running, delete the lock directory the error names.
48
+
49
+ ### Ledger "changed on disk since it was read"
50
+
51
+ `ledger for RUN-X changed on disk since it was read (another writer); re-read it and retry`: another session or process wrote the same Run's ledger just before you (two approvals at once, a `stop` racing a `resume`). The kernel refused this write; **the ledger was not changed and is not corrupted**. Run the same command again: it decides afresh on the current state (and may simply tell you it is already approved or terminal). Approve, reject, `--adopt`, `stop`, `resume` and supersede all read the ledger only after taking the Run lock, so ordinary use never meets this error; seeing it means there really was a concurrent operation.
52
+
53
+ ### Abnormal worker behaviour
54
+
55
+ - **Worker infrastructure failure (iteration 09)**: a timeout, a crash, output that is not an envelope (`invalid-output`), or the worker ending itself with exit code **75** (`EX_TEMPFAIL`, "environment unavailable"): the kernel classifies it as `infra`: no failure fingerprint is recorded, no fixer is dispatched, **the step's budget is not consumed**, the Run stops at `WAITING_HUMAN` (kind `infra`, transition `resume-<step>`), notifications go out as usual. Once the backend is back, `approve --transition resume-<step>` reruns the step; `reject` ends the Run. Real incidents: a worker backend returning 404 and non-JSON output killed five Runs in two days while the driving session hand-wrote a probe every two minutes; a missing rg on PATH, a port collision and a host load of 280 each dispatched a fixer.
56
+ - **A failure with no transition edge** (such as `failed` on build / review / fix in the preset) no longer ends in FAILED; it stops at `resume-<step>` as well, and a human decides whether to rerun or end.
57
+ - Out-of-scope writes → the Run BLOCKs and no candidate is pinned: check `allowedPaths` and the scope declared in the worker prompt;
58
+ - Timeouts → first check whether it is the environment (`infra` already stopped for you), then adjust `timeoutMs`; an exhausted budget is a brake, not a fault: approve `resume-<step>` to grant one more attempt, or narrow the scope.
59
+
60
+ ### The observe plane
61
+
62
+ - A probe stays `unverified`: fix the probe's reachability first; unverified means "could not collect", not "no problem";
63
+ - False alarms flooding: `observe triage --action dismiss`; the same fingerprint stays out of the queue until its severity rises;
64
+ - Observe cycle counts reset to zero after the runtime was deleted: normal; triage memory lives in the Git-plane drafts, and suppression keeps working (tested).
65
+
66
+ ### Everything is a mess
67
+
68
+ ```bash
69
+ rm -rf .buildbeat/runtime/
70
+ ```
71
+
72
+ Then start again from the Git plane. Any phenomenon where a long-term metric or a terminal-state explanation depends on the runtime is a bug; please report it.
73
+
74
+ ## Diagnostic entry point
75
+
76
+ `buildbeat doctor --config <run-config>`: the config parses, the workflow has no exit loop, adapter env posture, digests can be computed, supersede and stall thresholds, whether the notification channel and its environment variable are in place. `events`/`replay`/`metrics` are all read-only and can run at any time.
77
+
78
+ ## "Is it stuck?"
79
+
80
+ > Since 2.0.0-beta.4 (iteration 08).
81
+ Look at `buildbeat status --repo . --run <RUN>` first: the step in flight shows elapsed time, the repository's historical median, the worker command, how long ago the last output was and its last three lines. No output beyond the threshold (default 15 minutes, `--stall-after <minutes>` or `stallAfterMs` in the run config) marks `STALLED`: **marked, never killed**. How to judge:
82
+
83
+ - Output keeps coming → wait (compare against `typical` to see whether it is far beyond the median);
84
+ - STALLED and the worker is an agent CLI → most likely a long reasoning stretch or waiting for an interaction that never comes; `stop --reason`, then rerun by the crash recovery path (the interrupted step reruns itself);
85
+ - STALLED and the worker is a script → read the last three lines; usually it waits on an external resource (port, lock, network).
86
+
87
+ To avoid watching the screen, subscribe to `STALLED` notifications ([Approval guide](07-approval-guide.en.md)). `watch --repo . --run <RUN> --once true` probes once by hand.
88
+
89
+ ## Cleanup: gc
90
+
91
+ > Since 2.0.0-beta.4 (iteration 08).
92
+ Terminal Runs leave worktrees, `run/*` branches and the occasional lock. `buildbeat gc --repo .` prints the plan by default, `--apply true` executes it:
93
+
94
+ - It touches only Runs that are **terminal and already compacted into a run-record** (the Git plane must have the record before the runtime plane is touched);
95
+ - Worktrees may be deleted (the commits are on the branch); a dirty worktree is left alone without `--force true`;
96
+ - A branch is deleted only when the candidate **is reachable from another ref** (merged / tagged / on the remote) or the Run produced no candidate; otherwise it reports "reachable only from this branch, kept": that branch is the last thread to the evidence;
97
+ - Leftover `locks/<RUN>.lock` of terminal Runs are cleaned; the `active-run` lock is reclaimed too when its owner process is gone (the plan names the owner), and kept with the reason when the owner is alive, on another host, or unrecorded.
98
+
99
+ gc never writes to the ledger (after the terminal state only `RUN_COMPACTED` is allowed), so it can run at any time and repeatedly.
@@ -1,9 +1,15 @@
1
1
  # 故障恢复手册
2
2
 
3
+ **简体中文** | [English](10-recovery.en.md)
4
+
3
5
  设计前提([`V2-PLAN.md`](../../V2-PLAN.md) 不变量 23):**`.buildbeat/runtime/` 整个目录随时可删**——已接受工件、Decision、Intent 草稿与分诊、已终结 Run 的压实记录全部活在 Git 面。"删了重建"是默认排障手段,不是最后手段。
4
6
 
5
7
  ## 症状 → 处置
6
8
 
9
+ ### 「run config … has N problem(s)」
10
+
11
+ run 配置写错了,Run 没有起跑、什么都没改。按清单逐条改(每条写明是哪个键、错在哪、最接近的正确拼写),改完再跑同一条命令;`buildbeat doctor --config …` 可以先单独核对。
12
+
7
13
  ### 台账报 corrupted
8
14
 
9
15
  `status`/`inbox` 出现 `LEDGER CORRUPTED after seq=N (<原因>)`:台账在最后一条合法事件处截断视图并**拒绝追加**——恢复是人的决定,不静默修复。
@@ -18,19 +24,31 @@
18
24
  buildbeat resume --config <run-config.yaml>
19
25
  ```
20
26
 
27
+ 使用 `start --attempt new` 自动编号时,`resume --config <run-config.yaml>` 会续跑该家族唯一未终态的 Run,并打印选中的 ID;也可用 `--run <RUN-ID>` 显式指定配置中的 Run 本身或 `<家族>-NN`(数字至少两位)。配置本身已有台账时优先使用该精确 ID。多个未终态 Run 会列出候选并要求用 `--run` 选择;没有未终态 Run 会报告最新一次的 ID 和终态,没有台账则明确说明。
28
+
21
29
  在途步会以 `crashed` 关闭(事实落账),然后**重跑该步本身**(beta.3 改):进程死掉不说明候选有问题,丢失的那次尝试照常计入该步预算,预算耗尽即停人工。此前的语义是把 crash 当步骤失败走 failure 边——真实事故(deploy-18):宿主工具超时杀掉 verify worker,crash 被路由去 fix,fixer 面对零 verifier 证据白烧一轮。工作树脏了仍然先停人工。带批准恢复时会做 candidate/plan 新鲜度检查,变了即 `APPROVAL_STALE` 转人工。恢复不了就删 runtime 重跑——候选分支与 Git 面记录不丢。
22
30
 
23
31
  **启动纪律**(同一事故的另一半):长于分钟级的 Run 必须以脱离宿主工具超时的方式启动(`nohup`/`setsid`),交互式 shell 里 `start` 会打印这条提醒。
24
32
 
25
33
  ### 锁卡住("another run is active")
26
34
 
27
- 上一个 Run 异常退出可能留下仓库锁:确认真的没有活动 Run 后
35
+ 每把锁记录持有者(进程号、主机名、获取时间、命令)。驱动进程被杀、被宿主超时结束或机器重启后,锁会残留;下一次 `resume` / `stop` / `start` 拿锁时,若持有者**在本机且进程已不存在**,自动回收并打印 `reclaimed stale lock <id> (owner pid … is gone)`,无需手删任何文件。所以驱动被杀后直接:
28
36
 
29
37
  ```bash
30
- buildbeat stop --repo . --run RUN-X --reason "crashed; releasing lock"
38
+ buildbeat resume --config <run-config.yaml>
31
39
  ```
32
40
 
33
- `stop` 落终态与理由;单纯锁残留也可删 `.buildbeat/runtime/` 后重来。
41
+ 内核走上文的崩溃恢复(`RUN_INTERRUPTED` → 重跑中断步);不想续跑就 `buildbeat stop --repo . --run RUN-X --reason "…"`。
42
+
43
+ 只有三种情况仍会报 `another run is active` / `already locked`,报错里写明持有者:
44
+
45
+ - **持有者还活着**:另一个 Run 真的在跑,等它结束;确认它卡死再结束该进程;
46
+ - **持有者在别的主机**(共享盘上的仓库):到那台机器处理,本机不会替它回收;
47
+ - **没有持有者信息**:旧版本 buildbeat 留下的锁,或拿锁瞬间崩溃;确认没有 buildbeat 进程在跑后,删除报错里给出的那个锁目录。
48
+
49
+ ### 台账「changed on disk since it was read」
50
+
51
+ `ledger for RUN-X changed on disk since it was read (another writer); re-read it and retry`:另一个会话或进程刚好在你之前写了同一个 Run 的台账(例如两边同时批准、一边 `stop` 一边 `resume`)。内核拒绝了这次写入,**台账没有被改动、也没有损坏**;重新执行同一条命令即可,它会基于最新状态重新判断(可能直接告诉你「已经批过了 / 已终态」)。批准、拒绝、`--adopt`、`stop`、`resume` 与自动取代都在拿到 Run 锁之后才读台账,正常使用不会遇到这条报错;看到它说明确实有并发操作。
34
52
 
35
53
  ### Worker 行为异常
36
54
 
@@ -76,6 +94,6 @@ rm -rf .buildbeat/runtime/
76
94
  - 只动**终态且已压成 run-record** 的 Run(Git 面有账才动运行时面);
77
95
  - 工作树可删(提交都在分支上);脏工作树不带 `--force true` 不动;
78
96
  - 分支只在候选**已可从其他 ref 到达**(已合并 / 打 tag / 在远端)或 Run 未产出候选时删;否则明示"仅此分支可达,保留"——它是证据的最后一根线;
79
- - 终态 Run 的残留 `locks/<RUN>.lock` 一并清;`active-run` 锁仍按上文人工处置。
97
+ - 终态 Run 的残留 `locks/<RUN>.lock` 一并清;`active-run` 锁在持有者进程已不存在时一并回收(计划里写明持有者),持有者还活着 / 在别的主机 / 没有持有者信息时保留并说明原因。
80
98
 
81
99
  gc 永不写台账(终态后只允许 `RUN_COMPACTED`),所以随时可跑、可重复。
@@ -7,7 +7,7 @@
7
7
  | 文档 | 一句话 |
8
8
  |---|---|
9
9
  | [怎么和会话说话](00-how-to-talk.md) | **给用户看的**:项目从未开始到换期,每个阶段你说什么、会话做什么、你得到什么 |
10
- | [快速开始](01-quickstart.md) | 第一个 Run:装 `@latest` → 工作项与 run 配置 → accept → doctor → start → 看证据拍板;含失败分支 |
10
+ | [快速开始](01-quickstart.md) · [English](01-quickstart.en.md) | 第一个 Run:装 `@latest` → 工作项与 run 配置 → accept → doctor → start → 看证据拍板;含失败分支 |
11
11
  | [`templates/v2/`](../../../templates/v2/AGENTS.md) | 项目装载入口(AGENTS / CLAUDE / 指挥台 / BUILDBEAT 标记)、run 配置样板、信封 prompt 与 worker 包装 |
12
12
 
13
13
  在 AI 会话里用的人只需读第 0 篇;会话读 `SKILL.md` §0.5 驾驶手册。
@@ -16,10 +16,10 @@
16
16
 
17
17
  | 文档 | 一句话 |
18
18
  |---|---|
19
- | [Human Approval 指南](07-approval-guide.md) | inbox / approve / stale;接受、批准某转换、合并决定五词各指什么;分诊门;等待要能找到人;overview |
20
- | [Evidence 指南](06-evidence-guide.md) | 回读制证据、状态/等级、UNVERIFIED 文化、observe |
19
+ | [Human Approval 指南](07-approval-guide.md) · [English](07-approval-guide.en.md) | inbox / approve / stale;接受、批准某转换、合并决定五词各指什么;分诊门;等待要能找到人;overview |
20
+ | [Evidence 指南](06-evidence-guide.md) · [English](06-evidence-guide.en.md) | 回读制证据、状态/等级、UNVERIFIED 文化、observe |
21
21
  | [跨会话与团队接续](11-session-handoff.md) · [English](11-session-handoff.en.md) | 上下文落盘、关闭旧聊天、新成员接手、跨工具与跨机器边界 |
22
- | [故障恢复手册](10-recovery.md) | 台账损坏、Run 中断、infra 停人、锁、runtime 全删重建、gc |
22
+ | [故障恢复手册](10-recovery.md) · [English](10-recovery.en.md) | 台账损坏、Run 中断、infra 停人、锁、runtime 全删重建、gc |
23
23
 
24
24
  ## 配置参考
25
25
 
@@ -0,0 +1,13 @@
1
+ # 通知通道样例。URL 只能来自环境变量(这里是 BUILDBEAT_NOTIFY_URL),永远不写进 Git;
2
+ # 变量没设置就静默跳过、不影响 Run(fail-open)。【BOOTSTRAP】决策:简账先不接通知,
3
+ # 所以所有者没有 export 这个变量,等待只在 inbox 里;要开就 export 一个 webhook 地址。
4
+ kind: notify
5
+ version: 1
6
+ channels:
7
+ - id: owner-webhook
8
+ type: webhook
9
+ urlEnv: BUILDBEAT_NOTIFY_URL
10
+ events:
11
+ - HUMAN_REQUESTED
12
+ - RUN_TERMINAL
13
+ - STALLED
@@ -0,0 +1,31 @@
1
+ # observe 配置样例:一次 `buildbeat observe run --config .buildbeat/observe.yaml` = 一轮只读体检。
2
+ # 简账没有线上环境,这里只挂一个"测试还绿吗"的探针,示范 provider 的形状;真实项目换成部署平台的只读查询。
3
+ kind: workflow
4
+ version: 1
5
+ name: observe
6
+ providers:
7
+ - id: tests-green
8
+ command: bash
9
+ args:
10
+ - -lc
11
+ - npm test
12
+ schedule: manual
13
+ evidence:
14
+ kind: health
15
+ subject: jianzhang-tests
16
+ severity:
17
+ failed: error
18
+ unverified: warn
19
+ bands:
20
+ - level: log
21
+ when: "severity >= info"
22
+ - level: diagnose
23
+ when: "severity >= error"
24
+ - level: intent
25
+ when: "severity >= error"
26
+ triage:
27
+ actions:
28
+ - fix_now
29
+ - schedule
30
+ - dismiss
31
+ dismissFeedback: bands
@@ -0,0 +1,73 @@
1
+ # AGENTS.md — 简账 工作区 · BuildBeat 协作契约
2
+
3
+ > 本文件走开放标准 `AGENTS.md`,由工作区下的会话按各工具自己的方式装载(Claude Code / Codex / Cursor / Gemini CLI / Aider / Zed 等多数会自动读根目录 `AGENTS.md` 或 `CLAUDE.md`;不自动读的工具由人开场贴给会话——用哪个工具就按它的文档核对一次,不要假设)。目的:每个会话开工即知道「当前工作在哪 / 我是什么视角 / 读哪 / 写哪 / 该调哪条命令」,不靠人转述上下文。
4
+ > **层叠规则**(标准语义):会话从被编辑文件所在目录向上收集沿途所有 `AGENTS.md` 合并,**离得越近优先级越高**。本文件只写全局的(路由 / 协作规则 / 红线),各代码子仓的局部细节写进**该仓自己的 `AGENTS.md`**。
5
+ > 根目录 `CLAUDE.md` 只是一行指针(兼容只认该文件名的工具),内容单点在本文件。单仓项目,没有 `ARCHITECTURE.md` 与 `contracts/`。
6
+ > **本仓运行 BuildBeat**(运行时 `@haiyangbg/buildbeat@3.0.0`,`buildbeat` 由会话调用,人不必手敲)。
7
+
8
+ ## 0. 工作怎么发生(一页流程)
9
+
10
+ 1. **工作项**:每件事一个 `delivery/work/<WORK-ID>/`(`intent.md` 为什么做 + **止损线**(最多几个 Run / 几轮 review / 几小时,越线先问所有者"继续还是砍")+ `plan.md` 怎么做,可选 `env-facts.md` 记踩出来的环境事实);被 digest 绑定接受(`buildbeat accept`)前只是草稿、不产生义务。`overview` 的 `cost:` 行就是止损线的读数。
11
+ 2. **代码工作跑 Run**:`buildbeat start --config <run-config.yaml> --attempt new` → 隔离 worktree 内 Build→Verify→Fix→Review 自动闭环 → **停在合并决定**。push、合并、部署永远是人批之后的人类动作。
12
+ 3. **人怎么知道该做什么**:`buildbeat overview --repo .` 回答「每件事走到哪、下一步该谁」;`inbox` 只列等人批的 Run,每条后面附可复制的下一句命令;`status --run <RUN>` 回答「还在动吗、动了多久、卡没卡」。
13
+ 4. **上线**:生产动作是人的;`release-readback` 预设 + `release` 风险预设把「做之前回读 → 人做 → 做之后回读 → 观察 → 人关窗」记成 L4 证据,任一步失败即停人批。
14
+ 5. **observe 盯生产**:`buildbeat observe run --config .buildbeat/observe.yaml` 一次=一轮只读体检;异常分层(落账→只读诊断→intent 草稿入队 `delivery/observe/intents/`),草稿**绝不自动执行**,人用 `observe triage` 分诊。
15
+ 6. **拍板台账**:平台级真实决策包一行进 `pm/decisions.md`(从 `templates/pm/decisions.md` 拷,`pm/` 下只有这一个文件与可选的 `adr/`);Run 级批准落各 Work 的 `decisions.jsonl`;finding 裁决落 `review-findings.jsonl`。单仓项目没有 `contracts/`。
16
+ 7. **通知**:`.buildbeat/notify.yaml` 配一条通道(URL 只能来自环境变量),Run 停在人批 / 终态 / 疑似卡住会来找人。
17
+ 8. **打扫**:终态 Run 留下的工作树用 `buildbeat gc --repo .` 清(默认只出计划)。工作树在仓内 `.buildbeat/worktrees/`:`.gitignore` 排除 `.buildbeat/runtime/` 与 `.buildbeat/worktrees/`,测试框架的收集范围也要排除 `**/.buildbeat/**`(vitest `exclude`、jest `testPathIgnorePatterns`、pytest `norecursedirs`),否则主干测试会把旧候选的用例一起跑。
18
+ 9. **worker 信封**:`delivery/envelope/`(从 `templates/v2/envelope/` 拷)放 `worker.sh` 与 builder / reviewer / fixer 的 prompt,run 配置 `envelope.prompts` 指向它;换工具只改 run 配置里 `--` 后的命令。**worker 环境事实(写进 prompt)**:worker 的沙箱通常**不能监听端口**,需要起服务或绑定 loopback 的集成测试交给 verify 步,worker 只跑单测与静态检查,不要反复尝试;PATH 只认 POSIX 工具(`grep -E` 不用 `rg`,`find` 不用 `fd`)或在 `requires:` 里声明;verify / 包装脚本发现环境不满足(命令不在 PATH、端口被占、后端 404)就 `exit 75`,内核会当基础设施故障停人、不派 fixer、不扣预算。
19
+
20
+ ## 1. 工作包路由 —— Builder 端到端负责,会话按 AI 视角隔离
21
+
22
+ > 协作单元是需求/功能工作包(= Work)。一个 Builder 对工作包的产品判断、实现、测试、合并与发布证据端到端负责;下表是可调用的 AI 专业视角和文件写边界,不是人类岗位或固定交接流水线。共享事实走 Git(`delivery/` 与 Run 台账)。
23
+
24
+ | AI 视角 | cwd | 可写(拥有) | 只读 | 开工先读 |
25
+ |---|---|---|---|---|
26
+ | **产品**(规格/编排) | 仓根 | `delivery/**`、`pm/decisions.md` | 全仓 | `buildbeat overview --repo .` |
27
+ | **全栈**(实现) | 仓根 | `src/**`、`tests/**`(Run 内受 `allowedPaths` 机器约束) | `delivery/*` | 所属 Work 的 intent/plan + `run-config.yaml` |
28
+ | **测试**(独立核验) | 仓根 | `tests/**`、独立核验报告(落所属 Work 目录) | 实现 + 规格 | 所属 Work |
29
+
30
+ > 🔴 **边界**:新地盘 `src/export.js` 与 `tests/`;`src/ledger.js` 是记账核心,只维护、改动一律先问人;不得借道写入 `delivery/work/*/runs/`(内核写)。
31
+ > 🔴 **写者≠审者的机器化**:Run 内置 fresh-context 只读 reviewer(快照强制,写入即失败落账);merge 门绑定 candidate + plan + 证据 digest,过期即 stale。
32
+ > **开工/收工护栏**:任意会话开工先各仓 `git pull`,再 `buildbeat overview --repo .`(活动 Work、等人的 Run、成本);收工前再跑一次 `overview` 并把 warning / unverified 原样写进收口。生产状态问 `observe status`,不猜。
33
+
34
+ ## 1.5 UI 规范摘要(非 UI 项目可删)
35
+
36
+ - 延用既定设计语言与 token;每个可见界面处理加载 / 空 / 错误 / 移动端四态。
37
+ - **界面零元注释**:上线的可见界面不得出现给"做的人"看的文字;每次上线核查门必查。
38
+ - UI 交付的拍板对象必须含可渲染证据(真渲染入口 + 截图 digest);静态描述不构成拍板对象。
39
+
40
+ ## 2. 协作规则
41
+
42
+ **① 唯一入口** —— 活动工作看 `delivery/`(`overview` / `inbox`);不另建进度文件、状态文件或看板,进度由内核从台账与 Git 回读。
43
+ **② 接口先落盘再动手** —— 单仓项目没有跨边界契约;对外导出格式(CSV 列序、日期格式)的变化先写进所属 Work 的 plan 并被接受,再动代码。
44
+ **③ 交接靠 candidate hash + 台账** —— Run 停在合并决定时 candidate 已由 Git 回读固定;跨会话接力读 `delivery/work/<id>/` 即知全部事实,hash 不得编造。
45
+ **④ 护栏与不可逆动作** —— 开工 `overview`;部署/改契约/migration 等不可逆动作前再核一次并走人批;exit 0 不消除 `warning/unverified`。
46
+ **⑤ 风险分轨** —— Risk Preset:`fast`(仅 merge 人批)/ `standard`(plan+merge,默认)/ `controlled`(intent+plan+merge+release)/ `release`(上线回读车道)。
47
+ **⑥ 核查门** —— Run 内 reviewer 只读、结构化 findings;`reviewTriage: required` 时 P0/P1 先过人分诊再派 fixer;review 每 Run 默认 2 轮封顶。**完成 = hash + 可核验证据**;标准轨最低 L3,上线必须 L4。`UNVERIFIED` 永不当作通过。
48
+ **⑦ 状态单点** —— 事实进 Run 证据与 Work 记录;进度看 `overview`,度量看 `metrics`(本地只读)。
49
+ **⑧ 视觉问题带图对比** —— 提 UI bug 必附『实现截图 ⟷ 设计稿截图』并排 + 标注差异点。
50
+ **⑨ 单点事实** —— 线上版本只信实查(`observe status` / 部署平台);任何文档不写「当前线上 vX」;每个收敛后的真实决策包只在 `pm/decisions.md` 记一行;历史台账不回改。
51
+ **⑩ 真渲染拍板** —— 有 UI 的拍板对象必须是真渲染证据。
52
+ **⑪ 所有者可见命名进决策卡** —— 域名、服务名、环境名、自停时长、窗口时长等**所有者以后要看见或要念出来的名字与参数**,不由 worker 顺手定:进 intent 或门前决策卡(`BATCH_AT_GATE`),给推荐值和理由(用业务上听得懂的名字,不用内部术语)。
53
+
54
+ ## 2.5 任务包与人批节奏
55
+
56
+ **任务包信封** —— 开工时从用户目标与活动 Work 明确 `objective / in_scope / terminal_condition`(即 Work 的 intent/plan)。默认一个工作包覆盖多个子项;每个会话同时只认领一个。只要仍有安全、可逆、在范围内且能推进目标的工作,会话就继续做,不因子产物完成交还接力棒。
57
+
58
+ **域回复格式** —— 面向用户收口、交接或回复明确检查点时,统一写「已做 → 未做 → 下一步」;已做/未做默认各总结成一句,不习惯性 1234 分条;只有事项性质差异大时才列举,每条仍是"结果+证据"。
59
+
60
+ **审批三级**:`STOP_NOW`(跨发布门 / 扩范围 / 改冻结契约 / 不可逆外部动作 / 接受风险);`BATCH_AT_GATE`(冻结前可逆取舍、默认值、阈值、可见命名——攒到门前一次批 2~5 个);`NO_APPROVAL`(能实查的事实、派生约束、文档归档、普通 P2)。
61
+
62
+ **人批预算** —— 每个工作包、每道门默认 1 个 `BATCH_AT_GATE` 请求;待批项必须带 findings 摘要与风险声明。
63
+
64
+ > **元原则:能实查的不问人** —— 查代码 / 配置 / 部署平台 / `overview` / `status` / `observe status` 能得到的事实,不拿去问用户、不信文档、不信上游转述。
65
+
66
+ ## 3. 红线(每个会话受约束)
67
+
68
+ 1. **凭据不入 git、不出本机**:文档只标位置不写值;本地 .env gitignore + 600;机器闸 = gitleaks pre-commit;Worker 默认 env 白名单;通知 URL 只能来自环境变量。
69
+ 2. **不 `git add -A`**:只 stage 当前工作包拥有的具体文件;各仓分别提交。
70
+ 3. **不未授权部署**、不 force-push、不 `--amend` 已推送历史、不 `--no-verify`。Run 的合并决定只表示候选具备合并条件(`SUCCEEDED` ≠ 已合并),合并/push/发布是其后的人类动作、逐项授权。
71
+ 4. **每次部署完必更对应仓 `CHANGELOG.md`**;部署后 `observe run` 一轮。
72
+ 5. **写者≠审者**:Run 内置只读 reviewer 机器强制;写者转述不构成证据。
73
+ 6. **事实分层**:代码已确认事实 / 运行时待核事实 / 拟议需求 / 已实现行为,四类严格分开;未实查一律写「待核」。
@@ -0,0 +1,14 @@
1
+ # BUILDBEAT.md — 本项目的 BuildBeat 标记
2
+
3
+ **本项目运行 BuildBeat**:运行时 `@haiyangbg/buildbeat@3.0.0`(2026-09-09 首次接入;查看本机版本 `npm ls -g @haiyangbg/buildbeat`,查看最新 `npm view @haiyangbg/buildbeat@latest version`)
4
+ **装载方式**:会话读根目录 `AGENTS.md`(`CLAUDE.md` 是一行指针);驾驶手册在 BuildBeat Skill `SKILL.md` §0.5
5
+ **活动工作**:`delivery/work/<WORK-ID>/`(intent / plan / run-config / decisions.jsonl / runs/);信封与 worker 包装在 `delivery/envelope/`
6
+ 来源:<https://github.com/HaiYangBG1/BuildBeat>
7
+
8
+ ## 升级
9
+
10
+ 运行时升级只是 `npm install --global @haiyangbg/buildbeat@latest`,对项目文件零改动;升级后更新上面的版本行。模板(`AGENTS.md` / `指挥台.md` / `delivery/envelope/`)对照上游 [CHANGELOG.md](https://github.com/HaiYangBG1/BuildBeat/blob/main/CHANGELOG.md) 中「模板」条目手工同步,拿不准就让会话对比上游 `templates/v2/` 与本项目对应文件。事件 schema 只增不改,旧 Run 台账不需要迁移。
11
+
12
+ ## 回灌(比升级更重要)
13
+
14
+ 本项目踩到 BuildBeat **没覆盖的新坑**(新反模式 / 机制漏洞)→ 回上游 `lessons.md` 登记一条(症状 → 根因 → 解药),提 issue / PR 或直接改上游仓。只回灌真实事故,不回灌猜想。
@@ -0,0 +1,7 @@
1
+ # CLAUDE.md — 指针(🔴 勿在此处写内容)
2
+
3
+ 本工作区的会话路由、协作规则、红线,**单点在同目录的 [`AGENTS.md`](AGENTS.md)** —— 请立即读取那份。
4
+
5
+ > 本文件只为兼容「只认 `CLAUDE.md` 这个文件名的工具」而存在,**永远保持这几行**。
6
+ > 往这里复制任何规则 = 两份文档必然漂移(上游 `lessons.md` 第 1 条:SSOT 腐烂)。
7
+ > 也不要改成符号链接:Windows 上 git 默认 `core.symlinks=false`,clone 出来会静默退化成一个内容是路径字符串的普通文件,装载即失效。
@@ -0,0 +1,25 @@
1
+ # example/ —— 可直接拷的示例:虚构项目「简账」跑完一个 Work 的快照
2
+
3
+ > 这是一个**单仓、无 UI、一个 Builder 端到端**的最小项目,展示 `templates/` 填好项目事实之后长什么样,以及一个 Work 从立项、接受、Run、合并决定到合并之后,仓库里留下哪些文件。项目、人物、决策全部虚构。
4
+
5
+ ## 里面是什么
6
+
7
+ | 路径 | 是什么 | 来源 |
8
+ |---|---|---|
9
+ | `AGENTS.md` / `CLAUDE.md` / `指挥台.md` / `BUILDBEAT.md` | 会话装载入口、一行指针、给人看的操作卡、运行时版本标记 | `templates/v2/` 填好项目名、边界、视角路由 |
10
+ | `gitignore.template` | 拷入后改名 `.gitignore`(npm 包不带点文件,所以这里不带点) | `templates/gitignore.template` |
11
+ | `pm/decisions.md` | 平台级拍板台账:接入时的【BOOTSTRAP】决策包和本 Work 的【EXPORT-1】决策包各一行 | `templates/pm/decisions.md` |
12
+ | `.buildbeat/notify.yaml` / `observe.yaml` | 通知通道(URL 只走环境变量)与只读体检配置的样例 | 按 [Approval 指南](../docs/v2/guide/07-approval-guide.md) / [Evidence 指南](../docs/v2/guide/06-evidence-guide.md) |
13
+ | `delivery/envelope/` | worker 包装脚本 + builder / reviewer / fixer 三份 prompt,末尾补了本项目的环境事实 | `templates/v2/envelope/` |
14
+ | `delivery/work/WORK-EXPORT-DATE-FILTER/` | 一个完整的 Work:`intent.md`(为什么 + 止损线)、`plan.md`、`run-config.yaml`、`workflow.yaml`(官方预设的逐字副本)、`decisions.jsonl`、`runs/RUN-EXPORT-01/run-record.json` | 前四份人写;后两份是运行时产出(见下) |
15
+ | `src/` / `tests/` / `package.json` | 应用本体:`ledger.js` 是"老地盘"(只维护),`export.js` 是本 Work 加了日期筛选之后的样子,`npm test` 是 verify 步跑的真实命令 | 虚构 |
16
+
17
+ ## 快照的时点与哪些是机器写的
18
+
19
+ 快照 = **所有者批准合并决定并合并候选之后**的仓库。`decisions.jsonl` 里的三条(接受 intent、接受 plan、合并决定)和 `runs/RUN-EXPORT-01/run-record.json` 不是手写的:它们是把本目录放进一个一次性 Git 仓库、用脚本 worker 代替 `codex exec` 真跑一遍 `accept → doctor → start → approve` 得到的原始输出,只在拷出时去掉了本机路径(运行时本来就不输出绝对路径)。因此 run-record 里的 `candidate` / `base` 两个 commit 指向那个一次性仓库,在你拷出的仓库里查不到——真项目里它们必须可以 `git cat-file -t` 查到。review 的那条 P2 finding 也是脚本 worker 按 reviewer 合同返回的样例。
20
+
21
+ `tests/example-firstrun.test.js` 锁住两件事:这些工件彼此一致(plan 的 digest 与批准对象一致、无绝对路径),以及把本目录原样拷进一个新仓库后,用脚本 worker 能再起一个 Run(`RUN-EXPORT-02`)跑到合并决定,verify 步跑的是本项目真实的 `npm test`。它不证明某个真实模型能完成任务。
22
+
23
+ ## 怎么用它起自己的项目
24
+
25
+ 不要整目录照抄——项目名、边界、视角、验证命令都是简账的。按 [SKILL.md](../SKILL.md) §8 的"自查 → 少量提问 → 一屏确认 → 生成"走,让会话从 `templates/` 生成;本目录只用来对照"填好之后应该长什么样"。想手动核对命令,按 [快速开始](../docs/v2/guide/01-quickstart.md):把 `run-config.yaml` 里 `--` 后面的 `codex exec …` 换成你实际用的工具,`accept` 之后 `doctor`,再 `start --attempt new`。
@@ -0,0 +1,10 @@
1
+ 你是本 Run 的 builder,在一个隔离的 git worktree 里工作(当前目录)。
2
+
3
+ 1. 读环境变量 `BUILDBEAT_INPUT`(JSON):`workId` / `runId` / `step` / `attempt`。目标与计划在 `delivery/work/<workId>/intent.md` 与 `plan.md`——只做 plan 里写的,plan 没写的记到 `delivery/work/<workId>/notes.md` 交给人,不要顺手做。
4
+ 2. 只改 run 配置 `allowedPaths` 列出的目录(通常是 `src` 与 `tests`);越界改动不会成为候选。
5
+ 3. 不要 `git commit`、不要 `git push`、不要改分支:提交由包装脚本机械完成,push 已被封禁。
6
+ 4. 沙箱通常不能监听端口:需要起服务的集成测试交给 verify 步,你只跑单测与静态检查;PATH 只认 POSIX 工具(`grep -E` 不用 `rg`)。
7
+ 5. 所有者以后要看见或念出来的名字与参数(域名、服务名、环境名、时长)不由你定:写进 `delivery/work/<workId>/notes.md` 给推荐值与理由,等人批。
8
+ 6. 改完自检一次能跑的测试;把做了什么、没做什么、下一步各一句写到 stdout 末尾。
9
+
10
+ 本项目(简账)的环境事实:Node ≥ 20,测试命令 `npm test`(`node --test tests/*.test.js`),零依赖、不用装包;沙箱不能监听端口,本项目也没有需要端口的测试;`src/ledger.js` 是老地盘,只维护、不重构;PATH 只认 POSIX 工具。
@@ -0,0 +1,10 @@
1
+ 你是本 Run 的 fixer,在隔离的 git worktree 里工作(当前目录)。你只修被指出的问题,不做别的。
2
+
3
+ 1. 读环境变量 `BUILDBEAT_INPUT`(JSON):
4
+ - 有 `findings[]`:这是上一轮 review 的结论及裁决状态,**只修 `adjudication` 为 `accepted` 或 `open` 的条目**,`dismissed` 的不碰;
5
+ - 没有 `findings`:说明是 verify 失败进来的。失败日志在主仓 `.buildbeat/runtime/runs/<runId>/logs/verify-<n>.log`(`<n>` 是最近一次 verify 的 attempt;从 worktree 找主仓:`cd "$(git rev-parse --git-common-dir)/.."`),里面有命令、退出码、stdout / stderr。先读日志,再改代码。
6
+ 2. 只改 run 配置 `allowedPaths` 列出的目录;不要 `git commit` / `git push`(提交由包装脚本完成,push 已封禁)。
7
+ 3. 修不了或问题不在代码(环境缺工具、端口被占、后端 404):不要绕,`exit 75`,内核会当基础设施故障停人。
8
+ 4. 改完跑一次相关测试;把修了哪条、没修哪条及原因各一句写到 stdout 末尾。
9
+
10
+ 本项目(简账)的环境事实:Node ≥ 20,测试命令 `npm test`(`node --test tests/*.test.js`),零依赖、不用装包;沙箱不能监听端口,本项目也没有需要端口的测试;`src/ledger.js` 是老地盘,只维护、不重构;PATH 只认 POSIX 工具。
@@ -0,0 +1,13 @@
1
+ 你是本 Run 的 reviewer,只读:不要改任何文件、不要跑会写盘的命令(写入会被快照比对捕获并按失败落账)。
2
+
3
+ 1. 读环境变量 `BUILDBEAT_INPUT`(JSON)。若有 `lastReviewed.range`,只审 `git diff <range>` 的增量;否则审本分支相对 base 的全部改动(`git log --oneline` 与 `git diff HEAD~1` 起步,按需扩大)。
4
+ 2. `BUILDBEAT_INPUT.anchor` 是历史 finding 与人的裁决:**已裁决(accepted / dismissed)的结论不得翻案**;同一问题不要换措辞重提。
5
+ 3. 对照 `delivery/work/<workId>/plan.md`:做了 plan 没写的事、引入了未经批准的可见命名(域名 / 服务名 / 环境名)→ 记 P2。
6
+ 4. 严重度:P0 数据丢失 / 安全 / 不可逆;P1 功能错误或测试未覆盖计划要求的行为;P2 可维护性、命名、边界;P3 建议。只有 P0 / P1 会阻断。
7
+ 5. **最后只输出一个 JSON 对象,不要别的文字**(可以裹一层 ```json 代码栏):
8
+
9
+ ```json
10
+ {"status": "succeeded", "findings": [{"severity": "P1", "summary": "一句话,稳定、可复述、不带时间戳"}]}
11
+ ```
12
+
13
+ 没有问题就 `"findings": []`。每条必须有 `severity`(P0–P3)和 `summary`;缺字段或不是 JSON 会被当成 worker 故障停人,而不是候选缺陷。