@haiyangbg/buildbeat 3.2.0 → 3.3.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 +21 -0
- package/README.en.md +3 -3
- package/SKILL.md +5 -7
- package/docs/CAPABILITY-MATRIX.md +1 -1
- package/docs/README.md +1 -0
- package/docs/RELEASING.md +3 -3
- package/docs/v2/RFC-0001-product-definition.md +1 -1
- package/docs/v2/RFC-0002-domain-model.md +1 -1
- package/docs/v2/RFC-0003-workflow-policy.md +1 -1
- package/docs/v2/SPEC-0001-events-v1.md +2 -2
- package/docs/v2/guide/00-how-to-talk.en.md +61 -0
- package/docs/v2/guide/00-how-to-talk.md +2 -0
- 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 +127 -0
- package/docs/v2/guide/02-workflow-guide.md +7 -3
- package/docs/v2/guide/03-policy-guide.en.md +55 -0
- package/docs/v2/guide/03-policy-guide.md +2 -0
- package/docs/v2/guide/04-adapter-guide.en.md +66 -0
- package/docs/v2/guide/04-adapter-guide.md +2 -0
- package/docs/v2/guide/05-worker-contract.en.md +60 -0
- package/docs/v2/guide/05-worker-contract.md +2 -0
- package/docs/v2/guide/07-approval-guide.en.md +2 -2
- package/docs/v2/guide/07-approval-guide.md +4 -3
- package/docs/v2/guide/09-security-boundaries.en.md +41 -0
- package/docs/v2/guide/09-security-boundaries.md +3 -1
- package/docs/v2/guide/README.en.md +36 -0
- package/docs/v2/guide/README.md +8 -6
- 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/package.json +3 -3
- package/src/v2/cli/run.js +43 -22
- package/src/v2/presets/software-delivery.yaml +3 -5
- package/src/v2/runtime/decisions.js +16 -0
- package/src/v2/runtime/notify.js +1 -1
- package/src/v2/runtime/orchestrator.js +504 -404
- package/src/v2/runtime/overview.js +2 -1
- package/templates/v2/AGENTS.md +1 -1
- package/templates/v2/run-config.example.yaml +5 -7
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Adapter guide
|
|
2
|
+
|
|
3
|
+
[简体中文](04-adapter-guide.md) | **English**
|
|
4
|
+
|
|
5
|
+
Authority: [`RFC-0002 §Adapter`](../RFC-0002-domain-model.md) (Chinese); implementation: `src/v2/adapters/shell.js` (production), `src/v2/adapters/mock.js` (tests). Ruling #5: vendor-neutral — not tied to any agent vendor.
|
|
6
|
+
|
|
7
|
+
## Shell adapter: every CLI is a worker
|
|
8
|
+
|
|
9
|
+
`workers.<role>` in the run config is exactly one shell adapter configuration:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
workers:
|
|
13
|
+
builder:
|
|
14
|
+
command: codex
|
|
15
|
+
args:
|
|
16
|
+
- exec
|
|
17
|
+
- -s
|
|
18
|
+
- workspace-write
|
|
19
|
+
- <a prompt or script arguments>
|
|
20
|
+
timeoutMs: 900000
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- The working directory is the step's isolated worktree (not the main checkout);
|
|
24
|
+
- `args` support templates: `{workspace}` `{step}` `{worker}`;
|
|
25
|
+
- Proven workers: `codex exec` (four real M4 pilots) and any bash script; `claude -p` has the same shape and can be swapped in.
|
|
26
|
+
|
|
27
|
+
## Env allowlist (the default; part of removing capabilities)
|
|
28
|
+
|
|
29
|
+
A worker subprocess receives **only** `PATH HOME LANG LC_ALL TMPDIR TERM USER SHELL` by default — cloud credentials and token **environment variables** in the host shell cannot reach the worker (note that `HOME` is on the allowlist: credential **files** are outside this boundary; see [Security boundaries](09-security-boundaries.en.md)). `inheritEnv: true` opens it explicitly (doctor labels that isolation as ADVISORY only); single variables can be injected allowlist-style with `env:`:
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
workers:
|
|
33
|
+
verifier:
|
|
34
|
+
command: bash
|
|
35
|
+
env:
|
|
36
|
+
DATABASE_URL: postgres://localhost/app_test
|
|
37
|
+
CI: "1"
|
|
38
|
+
args:
|
|
39
|
+
- -lc
|
|
40
|
+
- npm test
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Values in `env:` must be scalars (turned into strings before they reach the subprocess) and variable names must be valid (letters, digits, underscore); otherwise `doctor` / `start` report an error while loading the config. **The CLI loading path of 2.0.0 and earlier dropped these two fields** (doctor reported the posture, but start ran with the default allowlist, and `env:` variables never reached the worker); since 2.0.1 they are passed through, with a CLI end-to-end regression (`tests/v2-run-cli.test.js`). API users who call `createShellAdapter` directly were not affected.
|
|
44
|
+
|
|
45
|
+
## Input and output
|
|
46
|
+
|
|
47
|
+
- Input: the `BUILDBEAT_INPUT` environment variable carries JSON (step / worker / candidate / failure summary and so on, per role; see the [Worker contract](05-worker-contract.en.md));
|
|
48
|
+
- Output: a step that needs a structured result (the reviewer and others) writes a JSON envelope to the path in `BUILDBEAT_OUTPUT`; codex can also write its last message with `-o` and have a wrapper script convert it;
|
|
49
|
+
- A pure command step (the verifier running tests) needs no envelope — the Runner reads back the exit code and logs as evidence.
|
|
50
|
+
|
|
51
|
+
## Result semantics
|
|
52
|
+
|
|
53
|
+
An adapter only reports facts: exitCode / signal / timedOut / spawnError / stdout / stderr / start and end time. The orchestrator writes the events; an adapter never touches kernel state. A timeout, a crash and a failure to start are recorded as `timeout` / `crashed` / the failure path respectively, each with an end-to-end test (`tests/v2-invariants.test.js`).
|
|
54
|
+
|
|
55
|
+
## Mock adapter
|
|
56
|
+
|
|
57
|
+
`createMockAdapter(script)`: gives each step a sequence of `"succeed"` / `"fail"` or `{behavior, envelope}`, for tests and evals; the behaviour cards are in [`evals/`](../../../evals/README.md).
|
|
58
|
+
|
|
59
|
+
## When to write a dedicated adapter
|
|
60
|
+
|
|
61
|
+
Only when the shell cannot express it (streaming interaction, a kept session) do you write a dedicated adapter; per the M3 ruling, connect everything through the shell first, and revisit only when a real pilot proves it is not enough.
|
|
62
|
+
|
|
63
|
+
## Live output
|
|
64
|
+
|
|
65
|
+
> Since 2.0.0-beta.4 (iteration 08).
|
|
66
|
+
When the orchestrator passes `liveDir` to the shell adapter, the subprocess's stdout/stderr are written straight to `<liveDir>/<step>-<attempt>.{stdout,stderr}.live` (the fds directly, without buffering in the parent), along with `live.json` (`step / attempt / worker / command / startedAt`). When the step returns, the adapter reads both streams back as `stdout` / `stderr` and deletes the live files — the result shape is unchanged and the evidence collector works as before. A custom adapter that wants `status` to show "last output N minutes ago" just produces files with the same names; without them `status` shows only the elapsed time.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Worker contract
|
|
2
|
+
|
|
3
|
+
[简体中文](05-worker-contract.md) | **English**
|
|
4
|
+
|
|
5
|
+
Authority: [`RFC-0003 §5`](../RFC-0003-workflow-policy.md) (Chinese; report B §8.1). A worker is a replaceable executor; the other half of the contract is always guaranteed physically by the Runner, never by the worker's good behaviour.
|
|
6
|
+
|
|
7
|
+
## General contract
|
|
8
|
+
|
|
9
|
+
- **Input**: the environment variable `BUILDBEAT_INPUT` (JSON): step, worker, run/work id, candidate (once pinned), allowed scope;
|
|
10
|
+
- **Output**: a step that needs a structured verdict writes a JSON envelope to the file named by `BUILDBEAT_OUTPUT`. The smallest valid example (matching the parser in `src/v2/runtime/orchestrator.js`; regression test `tests/v2-review-loop.test.js`):
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"status": "succeeded",
|
|
15
|
+
"findings": [
|
|
16
|
+
{"severity": "P1", "summary": "The date filter misses the end-date boundary, so records from that day are dropped."}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `status`: `succeeded` | `failed` | `blocked`;
|
|
22
|
+
- `findings[]` (optional; omitted means an empty array): each entry **must** have a `severity` (`P0` | `P1` | `P2` | `P3`) and a string `summary`; other fields are ignored and not recorded. A finding's fingerprint = severity + a normalised hash of `summary`, so `summary` should be stable and repeatable, without timestamps or random ids;
|
|
23
|
+
- **Blocking semantics**: `P0` / `P1` block — `findings-blocking` routes to fix (with `reviewTriage: required` it first stops for a human to triage); `P2` / `P3` are only recorded as evidence, do not block and dispatch no fixer; a fingerprint that has been dismissed no longer blocks ([Approval guide](07-approval-guide.en.md));
|
|
24
|
+
- **A format error is not a candidate defect**: one extra layer of markdown fences around the envelope (```json … ```) is tolerated; any other format (not JSON, not an object, a finding without `summary`, a severity outside P0–P3) = `invalid-output`, which since 2.0.0-beta.5 the kernel classifies as a worker infrastructure failure (kind `infra`): no fixer, no failure fingerprint, no budget charged, stop at `WAITING_HUMAN`; once the worker or environment is fixed, `approve --transition resume-<step>` continues;
|
|
25
|
+
- **What a worker says is not evidence**: the Runner trusts only facts it reads back itself (exit code, logs, git state); see the [Evidence guide](06-evidence-guide.en.md).
|
|
26
|
+
|
|
27
|
+
## Discipline per role
|
|
28
|
+
|
|
29
|
+
| Role | Write access | Key points of the contract |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| planner | The work directory | Produces intent/plan drafts; accepting them is a human, digest-bound act |
|
|
32
|
+
| builder | The isolated worktree (within `allowedPaths`) | Changes must land as git commits; an out-of-scope write = the Run BLOCKs and no candidate is pinned |
|
|
33
|
+
| verifier | Runs commands only | Runs the real tests; the exit code is the verdict; writes no envelope |
|
|
34
|
+
| fixer | Same as builder | The input is the last review's `findings[]` (with fingerprints and adjudication status; fix only accepted / open); when it is entered from a failed verify, the input carries **no** failure summary — the failed command / exit code / stdout / stderr are in the main checkout at `.buildbeat/runtime/runs/<RUN>/logs/verify-<attempt>.log`, and the prompt must say to read it ([template](../../../templates/v2/envelope/prompts/fixer.md)); a generic "check it again" is not accepted |
|
|
35
|
+
| reviewer | **None** (`readonly: true`) | Fresh-context and read-only; produces structured findings; any write to the worktree is caught by the snapshot comparison and recorded as a failure (invariant 9) |
|
|
36
|
+
|
|
37
|
+
## Failure and budget
|
|
38
|
+
|
|
39
|
+
A failed step is retried carrying its **failure fingerprint** (command + exit code + error summary + diff digest); the same fingerprint twice in a row, or going over `maxAttemptsPerStep` / the budget, stops and hands over to a human. The worker does not (and cannot) decide on its own to "try once more".
|
|
40
|
+
|
|
41
|
+
**No fixer configured does not mean automatic fixing**: when a role (commonly `fixer`) is missing from `workers:` in the run config, a Run that reaches that step neither errors nor skips it; it stops at `WAITING_HUMAN` (`enter-fix`, reason `no adapter configured for worker fixer; attended handoff`) and waits for a person. To get "fix automatically after a failing test", configure `fixer` (usually the same command as builder, with a prompt that reads the failure from `BUILDBEAT_INPUT`); see the [Quickstart](01-quickstart.en.md).
|
|
42
|
+
|
|
43
|
+
## Practical tips
|
|
44
|
+
|
|
45
|
+
- A ready-made wrapper script and three prompts are in [`templates/v2/envelope/`](../../../templates/v2/envelope/worker.sh): `worker.sh <role> -- <tool command…>` takes care of "exit 75 if the tool is not on PATH, append `$BUILDBEAT_PROMPT` as the last argument, commit mechanically after writing steps, write stdout to `$BUILDBEAT_OUTPUT` for read-only steps"; to switch tools, change only the command after `--`. The deterministic first-run regression is `tests/v2-templates-firstrun.test.js`, and the wrapper's shell contract is `tests/envelope-worker.test.sh`.
|
|
46
|
+
|
|
47
|
+
- Reference `delivery/work/<id>/plan.md` explicitly in the prompt, so the worker's goal is the very file whose digest was approved;
|
|
48
|
+
- The builder's commit can be done mechanically by the wrapper script (as in the M4 pilots: codex only edits files, and `git commit` happens in the wrapper);
|
|
49
|
+
- The reviewer's prompt asks for "the envelope JSON only", written to `$BUILDBEAT_OUTPUT` with `-o` / a redirect.
|
|
50
|
+
|
|
51
|
+
## Iteration 08: what the input gained
|
|
52
|
+
|
|
53
|
+
- `BUILDBEAT_PROMPT` (an environment variable, a file path) and `input.envelope` (`promptRef / file / digest / vars`): the prompt declared by `envelope:` in the run config has had its variables substituted by the kernel and been written to disk; the worker simply runs `cat "$BUILDBEAT_PROMPT"` instead of doing its own `git show`.
|
|
54
|
+
- `input.lastReviewed` (readonly steps only): `{candidate, run, evidenceRef, range}` — the candidate the last review saw and the `range` to the current candidate; a reviewer may review only the increment, but **settled verdicts must not be reopened** (`anchor` is still there).
|
|
55
|
+
- `input.findings` (writing steps) and `input.anchor` (readonly steps) are unchanged.
|
|
56
|
+
|
|
57
|
+
## Owner-visible names are not the worker's call
|
|
58
|
+
|
|
59
|
+
> Since 2.0.0-beta.4 (iteration 08).
|
|
60
|
+
Builders and planners tend to name things along the way: domains, service names, environment names, auto-stop durations, window durations. **Any name or parameter the owner will later see or say out loud is not an implementation detail but a decision for the gate**: put it in the intent, or collect it on the gate's decision card with a recommended value and the reason, and apply it only after a human approves. Real incident: a service named after an internal term took the owner four rounds of questions before it was renamed to a business name they understood. Say so in the prompt, and have the reviewer's checklist record "introduced an unapproved visible name" as P2.
|
|
@@ -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`:本轮有 finding 在修过之后又出现(同指纹,已 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
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Security and permission boundaries
|
|
2
|
+
|
|
3
|
+
[简体中文](09-security-boundaries.md) | **English**
|
|
4
|
+
|
|
5
|
+
Authority: [`RFC-0001 §Protected actions`](../RFC-0001-product-definition.md) (Chinese), [`V2-PLAN.md`](../../history/V2-PLAN.md) §9 invariants (Chinese). Design philosophy: **a protected action = a removed capability** — not "please, agent, don't", but making it impossible.
|
|
6
|
+
|
|
7
|
+
## Local boundaries on the Runner side (LOCAL_ENFORCED, all tested)
|
|
8
|
+
|
|
9
|
+
Each row has two columns: **what the kernel actually does**, and **what cannot be concluded from it**. The former has regression tests; the latter depends on the host's sandbox / container / server, and the Runner does not pretend otherwise.
|
|
10
|
+
|
|
11
|
+
| Boundary | What the kernel actually does (detection or removal) | What cannot be concluded from it |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Push blocked | Worktree-level `remote.pushurl=protected://push-blocked-by-buildbeat` — inside the workspace, a worker's `git push` to a configured remote has nowhere to go (tested against a real remote) | That a worker cannot `git remote add` another remote, or make any network request |
|
|
14
|
+
| Write scope | An out-of-scope write under `allowedPaths` → no candidate pinned, `workspace.scope` BLOCK recorded, the Run stops — an out-of-scope change **cannot** become a qualifying candidate | That the worker process cannot touch host directories outside the worktree |
|
|
15
|
+
| Read-only reviewer | A before/after snapshot comparison per step; any write to the worktree is recorded as a failure (invariant 9) — this is **detecting and blocking the result after the fact**, not an operating-system write ban | That the write is stopped the moment it happens |
|
|
16
|
+
| Credential isolation | The worker env allowlist is by default only `PATH HOME LANG LC_ALL TMPDIR TERM USER SHELL`; cloud-credential / token **environment variables** in the host shell do not reach the subprocess; opening it explicitly with `inheritEnv: true` is labelled ADVISORY by doctor; `env:` injects only the variables you name (really passed through on the CLI loading path since 2.0.1; in 2.0.0 and earlier doctor and start disagreed, see the [Adapter guide](04-adapter-guide.en.md)) | That a worker cannot read credential files under `$HOME`, the keychain, ssh keys or other host resources (`HOME` is on the allowlist) |
|
|
17
|
+
| One active Run | By default a repository-wide lock, so a repository drives one Run at a time; Works whose run configs set `parallel: true` may run in parallel with each other, and Runs of the same Work stay mutually exclusive (since 3.2.0) | That concurrency across repositories / machines is coordinated |
|
|
18
|
+
| Control files | Workflow / policy / run configs live in the main checkout, outside the worker worktree's write scope | That a worker cannot read them by other means |
|
|
19
|
+
| No external actions in the kernel | Merge, push, deploy and publish have **no call path** in the Runner (invariant 20, printed by doctor); at most the Runner puts "the candidate is fit to merge" in the inbox | That any external worker command you configure cannot do these things in every host environment |
|
|
20
|
+
|
|
21
|
+
In one sentence: **the kernel guarantees that an out-of-bounds result cannot enter the ledger, become a candidate or get stamped**; "the worker cannot do it in the first place" depends on the sandbox the host gives it (a tool allowlist, egress limits, no production credentials).
|
|
22
|
+
|
|
23
|
+
## Preconditions for running unattended (the stance enforced since the MVP)
|
|
24
|
+
|
|
25
|
+
Prompt injection is a first-class attack surface: an unattended worker consumes any file in the repository. An unattended run must satisfy all three layers at once; missing any layer downgrades it to attended (a human in the loop):
|
|
26
|
+
|
|
27
|
+
| Layer | Who guarantees it | What |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| Kernel | The Runner (the section above) | Push blocked, write scope, read-only reviewer, env allowlist, no call path for external actions |
|
|
30
|
+
| Host | Your worker sandbox / container / the tool's own permission mode (such as `codex exec -s read-only`) | A tool allowlist, egress limits, **no production credentials** — the kernel does not and cannot check this layer; doctor reports only the env posture |
|
|
31
|
+
| Server | Code hosting / CI / deployment platform | Branch protection, required CI, deployment approval (next section) |
|
|
32
|
+
|
|
33
|
+
observe's diagnose commands follow the same discipline: read-only, same env allowlist.
|
|
34
|
+
|
|
35
|
+
## SERVER_ENFORCED is an honest declaration
|
|
36
|
+
|
|
37
|
+
Branch protection, required CI and deployment approval are enforced by the server; marking a policy `SERVER_ENFORCED` says "this gate lives on the server", and the Runner records it without pretending it can guarantee it locally. Never mark as LOCAL what cannot be stopped locally.
|
|
38
|
+
|
|
39
|
+
## Credential red line (operations)
|
|
40
|
+
|
|
41
|
+
Credentials for publishing / deploying are read only at run time (for example from the macOS Keychain), never written to files, logs or Git; `doctor` checks the adapter's env posture. A configuration that breaks the red line should not pass review.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# 安全与权限边界
|
|
2
2
|
|
|
3
|
+
**简体中文** | [English](09-security-boundaries.en.md)
|
|
4
|
+
|
|
3
5
|
权威:[`RFC-0001 §保护动作`](../RFC-0001-product-definition.md)、[`V2-PLAN.md`](../../history/V2-PLAN.md) §9 不变量。设计哲学:**保护动作 = 能力移除**——不是"请 Agent 别做",而是让它做不到。
|
|
4
6
|
|
|
5
7
|
## Runner 侧的本地边界(LOCAL_ENFORCED,均有测试)
|
|
@@ -12,7 +14,7 @@
|
|
|
12
14
|
| 写范围 | `allowedPaths` 越界写入 → 不固定 candidate、`workspace.scope` BLOCK 落账、Run 停——越界改动**不可能**成为合格候选 | Worker 进程无法触碰 worktree 之外的宿主目录 |
|
|
13
15
|
| Reviewer 只读 | 步级前后快照比对,任何工作树写入按失败落账(不变量 9)——这是**事后检测并阻断结果**,不是操作系统级禁写 | 写入在发生那一刻就被拦下 |
|
|
14
16
|
| 凭据隔离 | Worker env 默认白名单仅 `PATH HOME LANG LC_ALL TMPDIR TERM USER SHELL`;宿主 shell 里的云凭据 / token **环境变量**不进子进程;`inheritEnv: true` 显式打开会被 doctor 标为 ADVISORY;`env:` 只注入你点名的变量(CLI 加载路径 2.0.1 起真正透传,2.0.0 及更早 doctor 与 start 姿态不一致,见 [Adapter 指南](04-adapter-guide.md)) | Worker 读不到 `$HOME` 下的凭据文件、keychain、ssh key 等宿主资源(`HOME` 在白名单里) |
|
|
15
|
-
| 单活动 Run |
|
|
17
|
+
| 单活动 Run | 默认仓库级锁,一仓同时只驱动一个 Run;run 配置 `parallel: true` 的 Work 之间可并行,同一 Work 的 Run 仍互斥(自 3.2.0) | 多仓 / 多机并发有协调 |
|
|
16
18
|
| 控制文件 | workflow / policy / run 配置在主检出,不在 Worker 的 worktree 写范围内 | Worker 无法通过其他途径读到它们 |
|
|
17
19
|
| 内核无外部动作 | merge、push、部署、发布在 Runner **没有调用路径**(不变量 20,doctor 打印);Runner 至多把"候选具备合并条件"放进 inbox | 你配置的任意外部 Worker 命令在全部宿主环境下都做不了这些动作 |
|
|
18
20
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# BuildBeat v2 documentation
|
|
2
|
+
|
|
3
|
+
[简体中文](README.md) | **English**
|
|
4
|
+
|
|
5
|
+
> The normative authority is the RFCs/SPEC ([`RFC-0001`](../RFC-0001-product-definition.md) product definition / [`RFC-0002`](../RFC-0002-domain-model.md) domain model / [`RFC-0003`](../RFC-0003-workflow-policy.md) workflow and policy / [`SPEC-0001`](../SPEC-0001-events-v1.md) event schema; all in Chinese); this directory is the operator's view, and where it conflicts with the implementation, the RFCs/SPEC and the code win — please report it. Design history (V2-PLAN, iteration records) is not in this directory.
|
|
6
|
+
|
|
7
|
+
## First use
|
|
8
|
+
|
|
9
|
+
| Document | In one line |
|
|
10
|
+
|---|---|
|
|
11
|
+
| [How to talk to a session](00-how-to-talk.en.md) | **For users**: from not started to the next phase, what you say at each stage, what the session does, what you get |
|
|
12
|
+
| [Quickstart](01-quickstart.en.md) | The first Run: install `@latest` → work item and run config → accept → doctor → start → read the evidence and decide; failure branches included |
|
|
13
|
+
| [`templates/v2/`](../../../templates/v2/AGENTS.md) | Project entry points (AGENTS / CLAUDE / command board / BUILDBEAT marker), run config sample, envelope prompts and the worker wrapper |
|
|
14
|
+
|
|
15
|
+
People working in an AI session only need document 0; the session reads the driving manual in `SKILL.md` §0.5.
|
|
16
|
+
|
|
17
|
+
## Day to day
|
|
18
|
+
|
|
19
|
+
| Document | In one line |
|
|
20
|
+
|---|---|
|
|
21
|
+
| [Human approval guide](07-approval-guide.en.md) | inbox / approve / stale; what accept, approving a transition and the merge decision each mean; the triage gate; waits must reach a person; overview |
|
|
22
|
+
| [Evidence guide](06-evidence-guide.en.md) | Read-back evidence, status/grade, the UNVERIFIED culture, observe |
|
|
23
|
+
| [Session and team handoff](11-session-handoff.en.md) | Writing context down, closing the old chat, a new member taking over, cross-tool and cross-machine boundaries |
|
|
24
|
+
| [Recovery](10-recovery.en.md) | Corrupted ledger, interrupted Run, infra stops, locks, rebuilding after deleting the runtime, gc |
|
|
25
|
+
|
|
26
|
+
## Configuration reference
|
|
27
|
+
|
|
28
|
+
| Document | In one line |
|
|
29
|
+
|---|---|
|
|
30
|
+
| [Workflow authoring guide](02-workflow-guide.en.md) | Step order, explicit transitions, readonly, terminal, budgets, cache, requires, parallel runs |
|
|
31
|
+
| [Policy guide](03-policy-guide.en.md) | Four policy types, 8 operators, three-valued logic, enforcement levels |
|
|
32
|
+
| [Adapter guide](04-adapter-guide.en.md) | Shell/Mock, the env allowlist and `env:` injection, plugging in any CLI agent, live output |
|
|
33
|
+
| [Worker contract](05-worker-contract.en.md) | The input/output envelope (`severity` + `summary`), blocking semantics, per-role discipline, the wrapper script |
|
|
34
|
+
| [Security and permission boundaries](09-security-boundaries.en.md) | What the kernel actually does vs what cannot be concluded from it; the three preconditions for running unattended |
|
|
35
|
+
|
|
36
|
+
observe v0 (probe → tiered response → intent drafts → human triage) is covered in [Quickstart §9](01-quickstart.en.md) and the [Evidence guide](06-evidence-guide.en.md); its frozen schema is [`RFC-0003 §8`](../RFC-0003-workflow-policy.md).
|
package/docs/v2/guide/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# BuildBeat v2 使用文档
|
|
2
2
|
|
|
3
|
+
**简体中文** | [English](README.en.md)
|
|
4
|
+
|
|
3
5
|
> 规范权威在 RFC/SPEC([`RFC-0001`](../RFC-0001-product-definition.md) 产品定位 / [`RFC-0002`](../RFC-0002-domain-model.md) 域模型 / [`RFC-0003`](../RFC-0003-workflow-policy.md) workflow 与 policy / [`SPEC-0001`](../SPEC-0001-events-v1.md) 事件 schema);本目录是操作视角,与实现冲突时以 RFC/SPEC 与代码为准并回报。设计历史(V2-PLAN、迭代记录)不在本目录。
|
|
4
6
|
|
|
5
7
|
## 第一次使用
|
|
6
8
|
|
|
7
9
|
| 文档 | 一句话 |
|
|
8
10
|
|---|---|
|
|
9
|
-
| [怎么和会话说话](00-how-to-talk.md) | **给用户看的**:项目从未开始到换期,每个阶段你说什么、会话做什么、你得到什么 |
|
|
11
|
+
| [怎么和会话说话](00-how-to-talk.md) · [English](00-how-to-talk.en.md) | **给用户看的**:项目从未开始到换期,每个阶段你说什么、会话做什么、你得到什么 |
|
|
10
12
|
| [快速开始](01-quickstart.md) · [English](01-quickstart.en.md) | 第一个 Run:装 `@latest` → 工作项与 run 配置 → accept → doctor → start → 看证据拍板;含失败分支 |
|
|
11
13
|
| [`templates/v2/`](../../../templates/v2/AGENTS.md) | 项目装载入口(AGENTS / CLAUDE / 指挥台 / BUILDBEAT 标记)、run 配置样板、信封 prompt 与 worker 包装 |
|
|
12
14
|
|
|
@@ -25,10 +27,10 @@
|
|
|
25
27
|
|
|
26
28
|
| 文档 | 一句话 |
|
|
27
29
|
|---|---|
|
|
28
|
-
| [Workflow 编写指南](02-workflow-guide.md) | 步序、显式转换、readonly、terminal、预算、cache、requires |
|
|
29
|
-
| [Policy 指南](03-policy-guide.md) | 四类 Policy、8 算子、三值逻辑、强制等级 |
|
|
30
|
-
| [Adapter 指南](04-adapter-guide.md) | Shell/Mock、env 白名单与 `env:` 注入、接任意 CLI Agent、实时输出 |
|
|
31
|
-
| [Worker 合同](05-worker-contract.md) | 输入输出信封(`severity` + `summary`)、阻断语义、各角色纪律、包装脚本 |
|
|
32
|
-
| [安全与权限边界](09-security-boundaries.md) | 内核实际做到的 vs 不能由此推出的;无人值守三层前置条件 |
|
|
30
|
+
| [Workflow 编写指南](02-workflow-guide.md) · [English](02-workflow-guide.en.md) | 步序、显式转换、readonly、terminal、预算、cache、requires、并行 Run |
|
|
31
|
+
| [Policy 指南](03-policy-guide.md) · [English](03-policy-guide.en.md) | 四类 Policy、8 算子、三值逻辑、强制等级 |
|
|
32
|
+
| [Adapter 指南](04-adapter-guide.md) · [English](04-adapter-guide.en.md) | Shell/Mock、env 白名单与 `env:` 注入、接任意 CLI Agent、实时输出 |
|
|
33
|
+
| [Worker 合同](05-worker-contract.md) · [English](05-worker-contract.en.md) | 输入输出信封(`severity` + `summary`)、阻断语义、各角色纪律、包装脚本 |
|
|
34
|
+
| [安全与权限边界](09-security-boundaries.md) · [English](09-security-boundaries.en.md) | 内核实际做到的 vs 不能由此推出的;无人值守三层前置条件 |
|
|
33
35
|
|
|
34
36
|
observe v0(探测→分层响应→Intent 草稿→人分诊)在 [快速开始 §9](01-quickstart.md) 与 [Evidence 指南](06-evidence-guide.md) 中覆盖;schema 冻结见 [`RFC-0003 §8`](../RFC-0003-workflow-policy.md)。
|
|
@@ -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`):修过的 finding 又出现(同指纹,已 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haiyangbg/buildbeat",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.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": {
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"check:docs": "bash tests/check-docs.sh",
|
|
28
28
|
"test": "node --test tests/*.test.js",
|
|
29
|
-
"test:
|
|
29
|
+
"test:envelope": "bash tests/envelope-worker.test.sh",
|
|
30
30
|
"test:plugin": "bash tests/plugin-marketplace.test.sh",
|
|
31
31
|
"test:pack-firstrun": "bash tests/pack-firstrun.test.sh",
|
|
32
32
|
"pack:check": "npm pack --dry-run",
|
|
33
|
-
"prepublishOnly": "npm test && npm run test:
|
|
33
|
+
"prepublishOnly": "npm test && npm run test:envelope && npm run test:plugin && npm run test:pack-firstrun && npm run check:docs && npm run pack:check"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=20"
|
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";
|
|
@@ -64,17 +64,17 @@ Usage:
|
|
|
64
64
|
buildbeat resume --config <run-config.yaml> [--run <RUN-ID>] [--adopt <sha> --by <name>] # --adopt: hand fix committed in the worktree; skip fix, resume at verify
|
|
65
65
|
buildbeat status --repo <path> --run <RUN-ID> [--stall-after <minutes>]
|
|
66
66
|
buildbeat inbox --repo <path>
|
|
67
|
-
buildbeat overview --repo <path> [--work <WORK-ID>] [--json
|
|
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>
|
|
74
|
-
buildbeat metrics --repo <path> [--json
|
|
74
|
+
buildbeat metrics --repo <path> [--json]
|
|
75
75
|
buildbeat stop --repo <path> --run <RUN-ID> --reason <text>
|
|
76
|
-
buildbeat gc --repo <path> [--apply
|
|
77
|
-
buildbeat watch --repo <path> --run <RUN-ID> [--stall-after <minutes>] [--interval <seconds>] [--once
|
|
76
|
+
buildbeat gc --repo <path> [--apply] [--force]
|
|
77
|
+
buildbeat watch --repo <path> --run <RUN-ID> [--stall-after <minutes>] [--interval <seconds>] [--once]
|
|
78
78
|
buildbeat observe run --config <observe.yaml>
|
|
79
79
|
buildbeat observe status --repo <path>
|
|
80
80
|
buildbeat observe triage --repo <path> --intent <ref> --action <fix_now|schedule|dismiss> [--by <name>] [--note <text>]
|
|
@@ -83,15 +83,33 @@ Usage:
|
|
|
83
83
|
buildbeat findings adjudicate --repo <path> --work <WORK-ID> --fingerprint <fp> --action <accept|dismiss> [--by <name>] [--note <text>]
|
|
84
84
|
`;
|
|
85
85
|
|
|
86
|
+
// Switches may stand alone (`--json`) or take an explicit true/false
|
|
87
|
+
// (`--json true`, the older spelling); every other flag needs a value.
|
|
88
|
+
const SWITCHES = new Set(["json", "apply", "force", "once"]);
|
|
89
|
+
|
|
86
90
|
function parseFlags(argv) {
|
|
87
91
|
const flags = {};
|
|
88
|
-
for (let index = 0; index < argv.length; index +=
|
|
92
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
89
93
|
const key = argv[index];
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
if (!key.startsWith("--") || key === "--") {
|
|
95
|
+
throw new Error(`unexpected argument: ${key}`);
|
|
96
|
+
}
|
|
97
|
+
const name = key.slice(2);
|
|
98
|
+
const next = argv[index + 1];
|
|
99
|
+
if (SWITCHES.has(name)) {
|
|
100
|
+
if (next === "true" || next === "false") {
|
|
101
|
+
flags[name] = next;
|
|
102
|
+
index += 1;
|
|
103
|
+
} else {
|
|
104
|
+
flags[name] = "true";
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
93
107
|
}
|
|
94
|
-
|
|
108
|
+
if (next === undefined || next.startsWith("--")) {
|
|
109
|
+
throw new Error(`${key} needs a value`);
|
|
110
|
+
}
|
|
111
|
+
flags[name] = next;
|
|
112
|
+
index += 1;
|
|
95
113
|
}
|
|
96
114
|
return flags;
|
|
97
115
|
}
|
|
@@ -700,12 +718,15 @@ function commandAccept(flags) {
|
|
|
700
718
|
if (!flags.repo || !flags.work || !flags.artifact) {
|
|
701
719
|
throw new Error("accept requires --repo, --work and --artifact");
|
|
702
720
|
}
|
|
703
|
-
const
|
|
721
|
+
const artifacts = String(flags.artifact).split(",").map((name) => name.trim()).filter(Boolean);
|
|
722
|
+
const results = acceptArtifacts(resolve(flags.repo), flags.work, artifacts, {
|
|
704
723
|
by: flags.by ?? "human",
|
|
705
724
|
});
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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");
|
|
709
730
|
}
|
|
710
731
|
|
|
711
732
|
// Artifacts a policy rule requires to be accepted (artifact.accepted leaves
|
|
@@ -773,20 +794,20 @@ function commandDoctor(flags) {
|
|
|
773
794
|
}
|
|
774
795
|
console.log("kernel capabilities: merge/deploy/publish have no call path in the runner (invariant 20)");
|
|
775
796
|
const budgetLines = [];
|
|
797
|
+
const reviewRounds = options.budgets.reviewRoundsPerWork ?? DEFAULT_REVIEW_ROUNDS_PER_WORK;
|
|
776
798
|
for (const step of options.workflow.steps) {
|
|
777
799
|
if (!step.worker) {
|
|
778
800
|
continue;
|
|
779
801
|
}
|
|
780
802
|
const fromRun = options.budgets.maxAttempts?.[step.id];
|
|
781
803
|
const fromPreset = options.workflow.budgets?.maxAttempts?.[step.id];
|
|
782
|
-
const
|
|
783
|
-
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";
|
|
784
807
|
budgetLines.push(`${step.id}=${effective} (${source})`);
|
|
785
808
|
}
|
|
786
809
|
console.log(`budgets (maxAttempts per step; approving resume-<step> after exhaustion grants +1): ${budgetLines.join(", ")}`);
|
|
787
|
-
|
|
788
|
-
console.log(`budgets.reviewRoundsPerWork: ${options.budgets.reviewRoundsPerWork} (counted across every run of the work, superseded ones included)`);
|
|
789
|
-
}
|
|
810
|
+
console.log(`budgets.reviewRoundsPerWork: ${reviewRounds}${options.budgets.reviewRoundsPerWork === undefined ? " (default)" : ""} (counted across every run of the work, superseded ones included)`);
|
|
790
811
|
// Same preconditions start's first gate will read (real incident, twice:
|
|
791
812
|
// doctor passed, start stopped at build because plan.md was not mirrored
|
|
792
813
|
// into the repository the run was started in).
|
|
@@ -1084,7 +1105,7 @@ function commandGc(flags) {
|
|
|
1084
1105
|
if (flags.apply !== "true") {
|
|
1085
1106
|
console.log(
|
|
1086
1107
|
actionable > 0
|
|
1087
|
-
? `plan only: ${actionable} action(s); rerun with --apply
|
|
1108
|
+
? `plan only: ${actionable} action(s); rerun with --apply to execute (branches whose candidate lives only there are always kept)`
|
|
1088
1109
|
: "nothing to collect",
|
|
1089
1110
|
);
|
|
1090
1111
|
return;
|
|
@@ -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 {
|