@haiyangbg/buildbeat 1.21.0 → 2.0.0-beta.2

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 (75) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.en.md +4 -4
  3. package/README.md +4 -4
  4. package/bin/buildbeat-v2.js +6 -0
  5. package/docs/BuildBeat v2/357/274/232AI /345/216/237/347/224/237/350/275/257/344/273/266/344/272/244/344/273/230/346/216/247/345/210/266/345/271/263/351/235/242.md" +2053 -0
  6. package/docs/CAPABILITY-MATRIX.md +4 -4
  7. package/docs/CLI.md +4 -4
  8. package/docs/EXECUTION-PLAN.md +1 -1
  9. package/docs/RELEASING.md +5 -5
  10. package/docs/ROADMAP.md +4 -1
  11. package/docs/V1.21-RELEASE-EVIDENCE-2026-08-25.md +55 -0
  12. package/docs/V2-D2-DECISION-CARD.md +37 -0
  13. package/docs/V2-DECISIONS.md +11 -0
  14. package/docs/V2-ITERATION-01.md +60 -0
  15. package/docs/V2-ITERATION-02.md +32 -0
  16. package/docs/V2-ITERATION-03.md +30 -0
  17. package/docs/V2-ITERATION-04.md +29 -0
  18. package/docs/V2-ITERATION-05.md +20 -0
  19. package/docs/V2-ITERATION-06.md +18 -0
  20. package/docs/V2-ITERATION-07.md +36 -0
  21. package/docs/V2-PLAN.md +333 -0
  22. package/docs/V2-PROPOSAL.md +319 -0
  23. package/docs/V2.0.0-BETA.1-RELEASE-EVIDENCE-2026-08-28.md +41 -0
  24. package/docs/v2/M1-ACCEPTANCE-2026-08-28.md +38 -0
  25. package/docs/v2/M2-DOD-2026-08-28.md +34 -0
  26. package/docs/v2/M4-CHICKAI-PILOT-2026-08-28.md +44 -0
  27. package/docs/v2/M4-EXTERNAL-PILOT-2026-08-28.md +46 -0
  28. package/docs/v2/M4-SELFHOST-2026-08-28.md +53 -0
  29. package/docs/v2/RFC-0001-product-definition.md +92 -0
  30. package/docs/v2/RFC-0002-domain-model.md +149 -0
  31. package/docs/v2/RFC-0003-workflow-policy.md +204 -0
  32. package/docs/v2/SPEC-0001-events-v1.md +98 -0
  33. package/docs/v2/guide/01-quickstart.md +92 -0
  34. package/docs/v2/guide/02-workflow-guide.md +42 -0
  35. package/docs/v2/guide/03-policy-guide.md +53 -0
  36. package/docs/v2/guide/04-adapter-guide.md +45 -0
  37. package/docs/v2/guide/05-worker-contract.md +37 -0
  38. package/docs/v2/guide/06-evidence-guide.md +38 -0
  39. package/docs/v2/guide/07-approval-guide.md +34 -0
  40. package/docs/v2/guide/08-migration-v1.md +68 -0
  41. package/docs/v2/guide/09-security-boundaries.md +28 -0
  42. package/docs/v2/guide/10-recovery.md +55 -0
  43. package/docs/v2/guide/README.md +18 -0
  44. package/example/.buildbeat/manifest.json +2 -2
  45. package/example/BUILDBEAT.md +1 -1
  46. package/package.json +4 -2
  47. package/src/constants.js +4 -1
  48. package/src/project.js +6 -1
  49. package/src/v2/adapters/mock.js +67 -0
  50. package/src/v2/adapters/shell.js +78 -0
  51. package/src/v2/cli/run.js +494 -0
  52. package/src/v2/domain/event-registry.js +100 -0
  53. package/src/v2/domain/model.js +61 -0
  54. package/src/v2/engine/reducer.js +253 -0
  55. package/src/v2/engine/risk-preset.js +48 -0
  56. package/src/v2/engine/workflow.js +201 -0
  57. package/src/v2/engine/yaml-subset.js +194 -0
  58. package/src/v2/evidence/collector.js +63 -0
  59. package/src/v2/observe/observe-config.js +194 -0
  60. package/src/v2/observe/observe-reducer.js +117 -0
  61. package/src/v2/observe/observe.js +420 -0
  62. package/src/v2/policy/policy.js +302 -0
  63. package/src/v2/presets/observe.yaml +45 -0
  64. package/src/v2/presets/policies/ui-render-gate.yaml +13 -0
  65. package/src/v2/presets/risk/controlled.yaml +39 -0
  66. package/src/v2/presets/risk/fast.yaml +19 -0
  67. package/src/v2/presets/risk/legacy-four-gates.yaml +44 -0
  68. package/src/v2/presets/risk/standard.yaml +28 -0
  69. package/src/v2/presets/software-delivery.yaml +39 -0
  70. package/src/v2/runtime/decisions.js +288 -0
  71. package/src/v2/runtime/metrics.js +140 -0
  72. package/src/v2/runtime/orchestrator.js +754 -0
  73. package/src/v2/runtime/run-record.js +55 -0
  74. package/src/v2/storage/event-ledger.js +154 -0
  75. package/src/v2/workspace/workspace-manager.js +146 -0
@@ -0,0 +1,420 @@
1
+ // observe v0 runner per docs/v2/RFC-0003-workflow-policy.md §8 (frozen
2
+ // semantics): providers produce contract-shaped Evidence (unreachable data is
3
+ // unverified, never silence); bands route log → readonly diagnose → intent
4
+ // DRAFT enqueue (never auto-executed); dismiss feeds back into drafting.
5
+ // Intent drafts and triage verdicts live in the Git plane so deleting
6
+ // .buildbeat/runtime/ loses no decision (invariant 23). One invocation runs
7
+ // one cycle; periodic operation is host-level (cron/CI) in v0.
8
+
9
+ import { createHash } from "node:crypto";
10
+ import {
11
+ existsSync,
12
+ mkdirSync,
13
+ readFileSync,
14
+ readdirSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { join, resolve } from "node:path";
18
+
19
+ import { createShellAdapter } from "../adapters/shell.js";
20
+ import { INTENT_DRAFT_STATES, TRIAGE_ACTIONS } from "../domain/model.js";
21
+ import { collectCommandEvidence } from "../evidence/collector.js";
22
+ import { EventLedger } from "../storage/event-ledger.js";
23
+ import { loadObserveConfig, severityRank } from "./observe-config.js";
24
+ import { OBSERVE_REDUCER } from "./observe-reducer.js";
25
+
26
+ const OBSERVE_IDS = { run: "OBSERVE", work: "OBSERVE" };
27
+ const KERNEL_ACTOR = { kind: "kernel", id: "observe" };
28
+ const HEADER_PREFIX = "<!-- buildbeat-intent ";
29
+ const HEADER_SUFFIX = " -->";
30
+
31
+ export class ObserveError extends Error {
32
+ constructor(message) {
33
+ super(message);
34
+ this.name = "ObserveError";
35
+ }
36
+ }
37
+
38
+ function observeLedgerPath(repoRoot) {
39
+ return join(repoRoot, ".buildbeat", "runtime", "observe", "events.jsonl");
40
+ }
41
+
42
+ export function openObserveLedger(repoRoot) {
43
+ return EventLedger.open(observeLedgerPath(repoRoot), OBSERVE_REDUCER);
44
+ }
45
+
46
+ function intentsDir(repoRoot) {
47
+ return join(repoRoot, "delivery", "observe", "intents");
48
+ }
49
+
50
+ function parseIntentHeader(filePath, text) {
51
+ const firstLine = text.split("\n", 1)[0] ?? "";
52
+ if (!firstLine.startsWith(HEADER_PREFIX) || !firstLine.endsWith(HEADER_SUFFIX)) {
53
+ throw new ObserveError(
54
+ `intent draft ${filePath} has no buildbeat-intent header line; fix or remove the file`,
55
+ );
56
+ }
57
+ const body = firstLine.slice(HEADER_PREFIX.length, -HEADER_SUFFIX.length);
58
+ let header;
59
+ try {
60
+ header = JSON.parse(body);
61
+ } catch {
62
+ throw new ObserveError(`intent draft ${filePath} header is not valid JSON`);
63
+ }
64
+ if (!INTENT_DRAFT_STATES.includes(header.status)) {
65
+ throw new ObserveError(
66
+ `intent draft ${filePath} has unknown status: ${header.status}`,
67
+ );
68
+ }
69
+ return header;
70
+ }
71
+
72
+ export function readIntentDrafts(repoRoot) {
73
+ const dir = intentsDir(repoRoot);
74
+ if (!existsSync(dir)) {
75
+ return [];
76
+ }
77
+ const drafts = [];
78
+ for (const name of readdirSync(dir).sort()) {
79
+ if (!name.endsWith(".md")) {
80
+ continue;
81
+ }
82
+ const filePath = join(dir, name);
83
+ const text = readFileSync(filePath, "utf8");
84
+ drafts.push({ file: name, path: filePath, header: parseIntentHeader(filePath, text), text });
85
+ }
86
+ return drafts;
87
+ }
88
+
89
+ function renderHeader(header) {
90
+ return `${HEADER_PREFIX}${JSON.stringify(header)}${HEADER_SUFFIX}`;
91
+ }
92
+
93
+ function writeDraft(filePath, header, body) {
94
+ writeFileSync(filePath, `${renderHeader(header)}\n${body}`, "utf8");
95
+ }
96
+
97
+ function rewriteHeader(draft, header, appendLine) {
98
+ const lines = draft.text.split("\n");
99
+ lines[0] = renderHeader(header);
100
+ let text = lines.join("\n");
101
+ if (appendLine) {
102
+ text = `${text.replace(/\n+$/, "\n")}${appendLine}\n`;
103
+ }
104
+ writeFileSync(draft.path, text, "utf8");
105
+ }
106
+
107
+ function fingerprintFor(provider, status) {
108
+ const key = [provider.id, provider.evidence.kind, provider.evidence.subject, status].join("|");
109
+ return createHash("sha256").update(key, "utf8").digest("hex").slice(0, 16);
110
+ }
111
+
112
+ function severityFor(provider, status) {
113
+ if (status === "passed") {
114
+ return null;
115
+ }
116
+ return status === "failed" ? provider.severity.failed : provider.severity.unverified;
117
+ }
118
+
119
+ function execCommand({ name, command, args, timeoutMs, repoRoot, input }) {
120
+ const adapter = createShellAdapter({ name, command, args, timeoutMs });
121
+ return adapter.execute({
122
+ step: name,
123
+ worker: name,
124
+ workspacePath: repoRoot,
125
+ input,
126
+ timeoutMs,
127
+ });
128
+ }
129
+
130
+ function recordEvidence({ ledger, config, cycle, provider, execResult, kind, step, grade = "L2" }) {
131
+ const record = collectCommandEvidence({
132
+ runtimeDir: join(config.repoRoot, ".buildbeat", "runtime", "observe"),
133
+ runId: `cycle-${String(cycle).padStart(4, "0")}`,
134
+ step,
135
+ attempt: 1,
136
+ execResult,
137
+ subject: provider.evidence.subject,
138
+ kind,
139
+ grade,
140
+ });
141
+ ledger.append({
142
+ type: "EVIDENCE_RECORDED",
143
+ actor: { kind: "provider", id: provider.id },
144
+ data: {
145
+ evidenceRef: record.location,
146
+ kind: record.kind,
147
+ subject: record.subject,
148
+ digest: record.digest,
149
+ status: record.status,
150
+ grade: record.grade,
151
+ provider: provider.id,
152
+ cycle,
153
+ },
154
+ ...OBSERVE_IDS,
155
+ });
156
+ return record;
157
+ }
158
+
159
+ function draftBody({ provider, severity, record, fingerprint }) {
160
+ return `# Intent 草稿:${provider.id} 在 ${provider.evidence.subject} 上发现 ${provider.evidence.kind} 异常
161
+
162
+ - 来源 provider:\`${provider.id}\`(severity ${severity})
163
+ - 主题:\`${provider.evidence.subject}\`
164
+ - 证据:\`${record.digest}\`(${record.status};日志见 runtime observe 面,runtime 可删,digest 永续)
165
+ - 指纹:\`${fingerprint}\`
166
+
167
+ ## 下一步(人分诊后才会发生任何执行)
168
+
169
+ - \`fix_now\`:接受后由人工带入 software-delivery Run(本草稿绝不自动执行);
170
+ - \`schedule\`:排期处理;
171
+ - \`dismiss\`:驳回——同指纹在严重度升级前不再重复入队。
172
+
173
+ 分诊命令:
174
+
175
+ \`\`\`
176
+ node src/v2/cli/run.js observe triage --repo <repo> --intent <本文件相对路径> --action fix_now|schedule|dismiss --by <name>
177
+ \`\`\`
178
+ `;
179
+ }
180
+
181
+ function handleIntentBand({ ledger, config, cycle, provider, severity, record, fingerprint, now }) {
182
+ const dir = intentsDir(config.repoRoot);
183
+ const drafts = readIntentDrafts(config.repoRoot);
184
+ const existing = drafts.find((draft) => draft.header.fingerprint === fingerprint);
185
+ if (existing) {
186
+ const header = existing.header;
187
+ if (header.status === "dismissed") {
188
+ if (severityRank(severity) <= severityRank(header.dismissedSeverity ?? header.severity)) {
189
+ return { outcome: "suppressed", intentRef: relativeIntentRef(config.repoRoot, existing.path) };
190
+ }
191
+ const reopened = {
192
+ ...header,
193
+ status: "draft",
194
+ severity,
195
+ evidenceDigest: record.digest,
196
+ updated: now,
197
+ };
198
+ delete reopened.dismissedSeverity;
199
+ delete reopened.triage;
200
+ rewriteHeader(
201
+ existing,
202
+ reopened,
203
+ `> ${now}:严重度升级为 ${severity}(此前以 ${header.dismissedSeverity ?? header.severity} 被 dismiss),草稿重新开启。`,
204
+ );
205
+ const intentRef = relativeIntentRef(config.repoRoot, existing.path);
206
+ ledger.append({
207
+ type: "INTENT_DRAFTED",
208
+ actor: KERNEL_ACTOR,
209
+ data: { intentRef, provider: provider.id, severity, fingerprint },
210
+ ...OBSERVE_IDS,
211
+ });
212
+ return { outcome: "reopened", intentRef };
213
+ }
214
+ if (severityRank(severity) > severityRank(header.severity)) {
215
+ rewriteHeader(existing, { ...header, severity, evidenceDigest: record.digest, updated: now });
216
+ }
217
+ return { outcome: "already-queued", intentRef: relativeIntentRef(config.repoRoot, existing.path) };
218
+ }
219
+ mkdirSync(dir, { recursive: true });
220
+ const fileName = `INTENT-${fingerprint}.md`;
221
+ const filePath = join(dir, fileName);
222
+ const header = {
223
+ id: fingerprint,
224
+ fingerprint,
225
+ provider: provider.id,
226
+ kind: provider.evidence.kind,
227
+ subject: provider.evidence.subject,
228
+ severity,
229
+ status: "draft",
230
+ evidenceDigest: record.digest,
231
+ created: now,
232
+ updated: now,
233
+ };
234
+ writeDraft(filePath, header, draftBody({ provider, severity, record, fingerprint }));
235
+ const intentRef = relativeIntentRef(config.repoRoot, filePath);
236
+ ledger.append({
237
+ type: "INTENT_DRAFTED",
238
+ actor: KERNEL_ACTOR,
239
+ data: { intentRef, provider: provider.id, severity, fingerprint },
240
+ ...OBSERVE_IDS,
241
+ });
242
+ return { outcome: "drafted", intentRef };
243
+ }
244
+
245
+ function relativeIntentRef(repoRoot, filePath) {
246
+ return filePath.startsWith(repoRoot) ? filePath.slice(repoRoot.length + 1) : filePath;
247
+ }
248
+
249
+ export function runObserveCycle({ configPath, now = new Date().toISOString() }) {
250
+ const config = loadObserveConfig(configPath);
251
+ const ledger = openObserveLedger(config.repoRoot);
252
+ const cycle = ledger.state.cycles + 1;
253
+ ledger.append({
254
+ type: "OBSERVE_CYCLE_STARTED",
255
+ actor: KERNEL_ACTOR,
256
+ data: { cycle, configDigest: config.configDigest },
257
+ ...OBSERVE_IDS,
258
+ });
259
+ const results = [];
260
+ for (const provider of config.providers) {
261
+ const execResult = execCommand({
262
+ name: `provider:${provider.id}`,
263
+ command: provider.command,
264
+ args: provider.args,
265
+ timeoutMs: provider.timeoutMs ?? 600000,
266
+ repoRoot: config.repoRoot,
267
+ input: { provider: provider.id, cycle },
268
+ });
269
+ const record = recordEvidence({
270
+ ledger,
271
+ config,
272
+ cycle,
273
+ provider,
274
+ execResult,
275
+ kind: provider.evidence.kind,
276
+ step: provider.id,
277
+ });
278
+ const severity = severityFor(provider, record.status);
279
+ const result = {
280
+ provider: provider.id,
281
+ status: record.status,
282
+ severity,
283
+ bands: [],
284
+ intent: null,
285
+ };
286
+ if (severity !== null) {
287
+ const fingerprint = fingerprintFor(provider, record.status);
288
+ for (const band of config.bands) {
289
+ if (severityRank(severity) < severityRank(band.minSeverity)) {
290
+ continue;
291
+ }
292
+ ledger.append({
293
+ type: "BAND_TRIGGERED",
294
+ actor: KERNEL_ACTOR,
295
+ data: { provider: provider.id, band: band.level, severity, fingerprint },
296
+ ...OBSERVE_IDS,
297
+ });
298
+ result.bands.push(band.level);
299
+ if (band.level === "diagnose") {
300
+ const diagnoseResult = provider.diagnose
301
+ ? execCommand({
302
+ name: `diagnose:${provider.id}`,
303
+ command: provider.diagnose.command,
304
+ args: provider.diagnose.args,
305
+ timeoutMs: provider.diagnose.timeoutMs ?? 600000,
306
+ repoRoot: config.repoRoot,
307
+ input: { provider: provider.id, cycle, band: "diagnose" },
308
+ })
309
+ : {
310
+ adapter: "observe",
311
+ command: "(no diagnose command configured)",
312
+ exitCode: null,
313
+ signal: null,
314
+ stdout: "",
315
+ stderr: "diagnose band triggered but the provider configures no diagnose command",
316
+ timedOut: false,
317
+ spawnError: "no diagnose command configured",
318
+ startedAt: now,
319
+ finishedAt: now,
320
+ };
321
+ recordEvidence({
322
+ ledger,
323
+ config,
324
+ cycle,
325
+ provider,
326
+ execResult: diagnoseResult,
327
+ kind: "diagnosis",
328
+ step: `${provider.id}-diagnose`,
329
+ });
330
+ }
331
+ if (band.level === "intent") {
332
+ result.intent = handleIntentBand({
333
+ ledger,
334
+ config,
335
+ cycle,
336
+ provider,
337
+ severity,
338
+ record,
339
+ fingerprint,
340
+ now,
341
+ });
342
+ }
343
+ }
344
+ }
345
+ results.push(result);
346
+ }
347
+ ledger.append({
348
+ type: "OBSERVE_CYCLE_FINISHED",
349
+ actor: KERNEL_ACTOR,
350
+ data: { cycle, providersRun: config.providers.length },
351
+ ...OBSERVE_IDS,
352
+ });
353
+ return { cycle, ledgerPath: ledger.path, results, state: ledger.state };
354
+ }
355
+
356
+ export function triageIntent({ repoRoot, intentRef, action, by = "unknown", note, now = new Date().toISOString() }) {
357
+ if (!TRIAGE_ACTIONS.includes(action)) {
358
+ throw new ObserveError(`action must be one of ${TRIAGE_ACTIONS.join("|")}, got: ${action}`);
359
+ }
360
+ const root = resolve(repoRoot);
361
+ const candidatePaths = [
362
+ resolve(root, intentRef),
363
+ join(intentsDir(root), intentRef),
364
+ join(intentsDir(root), `${intentRef}.md`),
365
+ ];
366
+ const filePath = candidatePaths.find((path) => existsSync(path));
367
+ if (!filePath) {
368
+ throw new ObserveError(`intent draft not found: ${intentRef}`);
369
+ }
370
+ const text = readFileSync(filePath, "utf8");
371
+ const header = parseIntentHeader(filePath, text);
372
+ const nextStatus = action === "fix_now" ? "accepted" : action === "schedule" ? "scheduled" : "dismissed";
373
+ const nextHeader = {
374
+ ...header,
375
+ status: nextStatus,
376
+ updated: now,
377
+ triage: { action, by, at: now, ...(note ? { note } : {}) },
378
+ ...(action === "dismiss" ? { dismissedSeverity: header.severity } : {}),
379
+ };
380
+ if (action !== "dismiss") {
381
+ delete nextHeader.dismissedSeverity;
382
+ }
383
+ rewriteHeader({ path: filePath, text }, nextHeader);
384
+ const ledger = openObserveLedger(root);
385
+ ledger.append({
386
+ type: "TRIAGE_RECORDED",
387
+ actor: { kind: "human", id: by },
388
+ data: {
389
+ intentRef: relativeIntentRef(root, filePath),
390
+ action,
391
+ fingerprint: header.fingerprint,
392
+ },
393
+ ...OBSERVE_IDS,
394
+ });
395
+ return {
396
+ intentRef: relativeIntentRef(root, filePath),
397
+ status: nextStatus,
398
+ fingerprint: header.fingerprint,
399
+ suggestion:
400
+ action === "fix_now"
401
+ ? "accepted: start a software-delivery run for this intent (run.js start --config <run-config.yaml>) — the draft never executes itself"
402
+ : null,
403
+ };
404
+ }
405
+
406
+ export function observeStatus({ repoRoot }) {
407
+ const root = resolve(repoRoot);
408
+ const ledger = openObserveLedger(root);
409
+ const drafts = readIntentDrafts(root);
410
+ return {
411
+ ledgerPath: ledger.path,
412
+ corruption: ledger.corruption,
413
+ cycles: ledger.state.cycles,
414
+ providers: ledger.state.providers,
415
+ intents: drafts.map((draft) => ({
416
+ file: relativeIntentRef(root, draft.path),
417
+ ...draft.header,
418
+ })),
419
+ };
420
+ }