@haiyangbg/buildbeat 3.1.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -10
- package/README.en.md +3 -3
- package/SKILL.md +19 -299
- package/docs/CAPABILITY-MATRIX.md +1 -1
- package/docs/README.md +6 -5
- package/docs/RELEASING.md +5 -5
- package/docs/v2/RFC-0001-product-definition.md +5 -5
- package/docs/v2/RFC-0002-domain-model.md +2 -2
- package/docs/v2/RFC-0003-workflow-policy.md +3 -3
- 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/02-workflow-guide.en.md +125 -0
- package/docs/v2/guide/02-workflow-guide.md +7 -1
- 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/09-security-boundaries.en.md +41 -0
- package/docs/v2/guide/09-security-boundaries.md +4 -2
- package/docs/v2/guide/10-recovery.en.md +1 -1
- package/docs/v2/guide/10-recovery.md +1 -1
- package/docs/v2/guide/README.en.md +36 -0
- package/docs/v2/guide/README.md +8 -6
- package/docs/v2/skill/01-principles.md +26 -0
- package/docs/v2/skill/02-project-layout.md +31 -0
- package/docs/v2/skill/03-collaboration-rules.md +45 -0
- package/docs/v2/skill/04-rhythm-and-rituals.md +102 -0
- package/docs/v2/skill/05-red-lines.md +13 -0
- package/docs/v2/skill/06-bootstrap-and-takeover.md +84 -0
- package/docs/v2/skill/07-templates-and-lessons.md +24 -0
- package/package.json +7 -16
- package/src/v2/cli/run-config-check.js +10 -4
- package/src/v2/cli/run.js +35 -11
- package/src/v2/engine/yaml-subset.js +17 -11
- package/src/v2/presets/policies/ui-render-gate.yaml +1 -1
- package/src/v2/runtime/decisions.js +1 -1
- package/src/v2/runtime/gc.js +41 -25
- package/src/v2/runtime/metrics.js +3 -2
- package/src/v2/runtime/orchestrator.js +542 -412
- package/src/v2/workspace/workspace-manager.js +62 -3
- package/templates/v2/CLAUDE.md +1 -1
- package/templates/v2/run-config.example.yaml +2 -0
package/src/v2/cli/run.js
CHANGED
|
@@ -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
70
|
buildbeat accept --repo <path> --work <WORK-ID> --artifact <plan|intent|spec> [--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;
|
|
107
|
+
}
|
|
108
|
+
if (next === undefined || next.startsWith("--")) {
|
|
109
|
+
throw new Error(`${key} needs a value`);
|
|
93
110
|
}
|
|
94
|
-
flags[
|
|
111
|
+
flags[name] = next;
|
|
112
|
+
index += 1;
|
|
95
113
|
}
|
|
96
114
|
return flags;
|
|
97
115
|
}
|
|
@@ -406,6 +424,7 @@ function loadRunConfig(flags, command) {
|
|
|
406
424
|
requires: config.requires ?? [],
|
|
407
425
|
reviewTriage: config.reviewTriage === "required" ? "required" : null,
|
|
408
426
|
supersede: config.supersede ?? "waiting",
|
|
427
|
+
parallel: config.parallel === true,
|
|
409
428
|
stallAfterMs: config.stallAfterMs !== undefined ? Number(config.stallAfterMs) : DEFAULT_STALL_AFTER_MS,
|
|
410
429
|
planDigest: digestOfWorkFile("plan.md"),
|
|
411
430
|
intentDigest: digestOfWorkFile("intent.md"),
|
|
@@ -531,7 +550,7 @@ async function commandStart(flags) {
|
|
|
531
550
|
console.error(`blocked by ${holder}: ${summary}`);
|
|
532
551
|
console.error(` watch it: buildbeat status --repo ${label} --run ${holder}`);
|
|
533
552
|
}
|
|
534
|
-
console.error("queue position: next after the holder(s) above stop or wait on a human (
|
|
553
|
+
console.error("queue position: next after the holder(s) above stop or wait on a human (by default one run drives a repository at a time; works whose run configs both set parallel: true can drive together)");
|
|
535
554
|
}
|
|
536
555
|
throw error;
|
|
537
556
|
}
|
|
@@ -829,6 +848,11 @@ function commandDoctor(flags) {
|
|
|
829
848
|
console.log("environment contract: none declared (implicit PATH facts stay unchecked)");
|
|
830
849
|
}
|
|
831
850
|
console.log(`supersede: ${options.supersede} (new run for the same work ${options.supersede === "off" ? "leaves" : "supersedes"} older WAITING_HUMAN runs)`);
|
|
851
|
+
console.log(
|
|
852
|
+
options.parallel
|
|
853
|
+
? "concurrency: parallel (runs of other works with parallel: true may drive at the same time; runs of this work stay exclusive; verifiers must not share fixed ports or databases)"
|
|
854
|
+
: "concurrency: exclusive (default: one driving run per repository; set parallel: true to drive alongside other works)",
|
|
855
|
+
);
|
|
832
856
|
console.log(`stall threshold: ${formatMs(options.stallAfterMs)} without worker output`);
|
|
833
857
|
const { config: notify, error: notifyError } = notifyConfigFor(options.repoRoot);
|
|
834
858
|
if (notifyError) {
|
|
@@ -1078,7 +1102,7 @@ function commandGc(flags) {
|
|
|
1078
1102
|
if (flags.apply !== "true") {
|
|
1079
1103
|
console.log(
|
|
1080
1104
|
actionable > 0
|
|
1081
|
-
? `plan only: ${actionable} action(s); rerun with --apply
|
|
1105
|
+
? `plan only: ${actionable} action(s); rerun with --apply to execute (branches whose candidate lives only there are always kept)`
|
|
1082
1106
|
: "nothing to collect",
|
|
1083
1107
|
);
|
|
1084
1108
|
return;
|
|
@@ -142,20 +142,26 @@ function parseList(lines, start, indent) {
|
|
|
142
142
|
}
|
|
143
143
|
const first = childLines[0].content;
|
|
144
144
|
const quotedScalar = first.startsWith('"') || first.startsWith("'");
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
line.lineNo,
|
|
152
|
-
);
|
|
145
|
+
if (childLines.length > 1) {
|
|
146
|
+
// Multi-line items keep the previous rule and messages unchanged.
|
|
147
|
+
if (!quotedScalar && first.includes(":")) {
|
|
148
|
+
result.push(parseMapFromLines(childLines, itemIndent));
|
|
149
|
+
} else {
|
|
150
|
+
throw new YamlSubsetError("unsupported list item shape", line.lineNo);
|
|
153
151
|
}
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
index = cursor;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!quotedScalar && MAP_ITEM.test(first)) {
|
|
156
156
|
result.push(parseMapFromLines(childLines, itemIndent));
|
|
157
|
+
} else if (!quotedScalar && (first.includes(": ") || first.endsWith(":"))) {
|
|
158
|
+
// Real YAML reads "echo a: b" as a map; rather than guess, ask for quotes.
|
|
159
|
+
throw new YamlSubsetError(
|
|
160
|
+
`list item ${JSON.stringify(first)} contains ": "; quote it (- ${JSON.stringify(first)}) or write it as key: value`,
|
|
161
|
+
line.lineNo,
|
|
162
|
+
);
|
|
157
163
|
} else {
|
|
158
|
-
|
|
164
|
+
result.push(parseScalar(first, childLines[0].lineNo));
|
|
159
165
|
}
|
|
160
166
|
index = cursor;
|
|
161
167
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Invariant 22 (
|
|
1
|
+
# Invariant 22 (lessons.md「静态稿拍板 → 返工螺旋」, machine form): a UI delivery's spec approval
|
|
2
2
|
# subject must include renderable proof — a screenshot with a digest —
|
|
3
3
|
# before a human may stamp it. Static prose is not an approvable design.
|
|
4
4
|
kind: policy
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// An approval is recorded only after re-reading the workspace and confirming
|
|
3
3
|
// the subject is still exactly what the request showed — if the candidate
|
|
4
4
|
// moved or the tree is dirty, the request is refreshed instead of stamped
|
|
5
|
-
// (lessons
|
|
5
|
+
// (lessons.md「读过期 race」: no rubber-stamping a moved target). Every decision lands both
|
|
6
6
|
// as a DECISION_RECORDED event and as a line in the Git plane
|
|
7
7
|
// (delivery/work/<work>/decisions.jsonl).
|
|
8
8
|
|
package/src/v2/runtime/gc.js
CHANGED
|
@@ -19,7 +19,14 @@ import { join } from "node:path";
|
|
|
19
19
|
import { execFileSync } from "node:child_process";
|
|
20
20
|
|
|
21
21
|
import { EventLedger } from "../storage/event-ledger.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
describeLockOwner,
|
|
24
|
+
inspectLock,
|
|
25
|
+
isRunLockName,
|
|
26
|
+
readback,
|
|
27
|
+
reclaimStaleLock,
|
|
28
|
+
withRepoGitLock,
|
|
29
|
+
} from "../workspace/workspace-manager.js";
|
|
23
30
|
import { resolveRepoRef } from "./repo-ref.js";
|
|
24
31
|
|
|
25
32
|
function git(cwd, args) {
|
|
@@ -65,18 +72,25 @@ export function planGc(repoRoot) {
|
|
|
65
72
|
const runsDir = join(repoRoot, ".buildbeat", "runtime", "runs");
|
|
66
73
|
const locksDir = join(repoRoot, ".buildbeat", "runtime", "locks");
|
|
67
74
|
const rows = [];
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
// Locks that belong to no single run (active-run, and the @work /
|
|
76
|
+
// @parallel / @repo-git locks): reclaimable only when their owner process
|
|
77
|
+
// is provably gone (same host, pid no longer exists).
|
|
78
|
+
const sharedLocks = existsSync(locksDir)
|
|
79
|
+
? readdirSync(locksDir)
|
|
80
|
+
.filter((entry) => entry.endsWith(".lock") && !isRunLockName(entry.slice(0, -".lock".length)))
|
|
81
|
+
.sort()
|
|
82
|
+
: [];
|
|
83
|
+
for (const entry of sharedLocks) {
|
|
84
|
+
const name = entry.slice(0, -".lock".length);
|
|
85
|
+
const lockPath = join(locksDir, entry);
|
|
86
|
+
const seen = inspectLock(lockPath);
|
|
87
|
+
const row = { run: "(repository)", status: `${name} lock`, actions: [], keep: [] };
|
|
74
88
|
if (seen.state === "dead") {
|
|
75
|
-
row.actions.push({ kind: "remove-lock", path:
|
|
89
|
+
row.actions.push({ kind: "remove-lock", path: lockPath, owner: seen.owner });
|
|
76
90
|
} else if (seen.state === "unknown") {
|
|
77
|
-
row.keep.push(
|
|
91
|
+
row.keep.push(`${name} lock has no owner record (older buildbeat?); remove it by hand once no buildbeat process is running`);
|
|
78
92
|
} else {
|
|
79
|
-
row.keep.push(
|
|
93
|
+
row.keep.push(`${name} lock held by ${describeLockOwner(seen.owner)}${seen.state === "foreign-host" ? " (another host)" : " (still running)"}`);
|
|
80
94
|
}
|
|
81
95
|
rows.push(row);
|
|
82
96
|
}
|
|
@@ -178,24 +192,26 @@ export function applyGc(repoRoot, rows, { force = false } = {}) {
|
|
|
178
192
|
result.error = "worktree dirty; rerun with --force true to discard";
|
|
179
193
|
continue;
|
|
180
194
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
195
|
+
withRepoGitLock(repoRoot, () => {
|
|
196
|
+
if (action.registered) {
|
|
197
|
+
const args = ["worktree", "remove"];
|
|
198
|
+
if (force || action.dirty) {
|
|
199
|
+
args.push("--force");
|
|
200
|
+
}
|
|
201
|
+
args.push(action.path);
|
|
202
|
+
git(repoRoot, args);
|
|
203
|
+
} else if (action.present) {
|
|
204
|
+
rmSync(action.path, { recursive: true, force: true });
|
|
185
205
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
git(repoRoot, ["worktree", "prune"]);
|
|
193
|
-
} catch {
|
|
194
|
-
// prune is best-effort
|
|
195
|
-
}
|
|
206
|
+
try {
|
|
207
|
+
git(repoRoot, ["worktree", "prune"]);
|
|
208
|
+
} catch {
|
|
209
|
+
// prune is best-effort
|
|
210
|
+
}
|
|
211
|
+
});
|
|
196
212
|
result.done = true;
|
|
197
213
|
} else if (action.kind === "delete-branch") {
|
|
198
|
-
git(repoRoot, ["branch", "-D", action.branch]);
|
|
214
|
+
withRepoGitLock(repoRoot, () => git(repoRoot, ["branch", "-D", action.branch]));
|
|
199
215
|
result.done = true;
|
|
200
216
|
}
|
|
201
217
|
} catch (error) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// buildbeat metrics v0: local, read-only, derived entirely from run ledgers.
|
|
2
|
-
// No collection, no upload (V2-PLAN §6
|
|
3
|
-
// forever doing precise work on
|
|
2
|
+
// No collection, no upload (V2-PLAN §6; lessons.md「流程只管"怎么做对",不管
|
|
3
|
+
// "做的是不是对的事"」: without numbers you are forever doing precise work on
|
|
4
|
+
// the wrong thing).
|
|
4
5
|
|
|
5
6
|
import { existsSync, readdirSync } from "node:fs";
|
|
6
7
|
import { join } from "node:path";
|