@haiyangbg/buildbeat 3.3.1 → 4.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 (102) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.en.md +20 -124
  3. package/README.md +20 -124
  4. package/SKILL.md +73 -84
  5. package/docs/CAPABILITY-MATRIX.md +30 -39
  6. package/docs/MIGRATION.md +93 -0
  7. package/docs/README.md +8 -37
  8. package/docs/RELEASING.md +4 -3
  9. package/docs/v2/RFC-0001-product-definition.md +3 -0
  10. package/docs/v2/RFC-0002-domain-model.md +3 -0
  11. package/docs/v2/RFC-0003-workflow-policy.md +3 -0
  12. package/docs/v2/SPEC-0001-events-v1.md +3 -0
  13. package/docs/v2/guide/00-how-to-talk.en.md +12 -56
  14. package/docs/v2/guide/00-how-to-talk.md +12 -56
  15. package/docs/v2/guide/01-quickstart.en.md +42 -111
  16. package/docs/v2/guide/01-quickstart.md +42 -111
  17. package/docs/v2/guide/02-workflow-guide.en.md +13 -123
  18. package/docs/v2/guide/02-workflow-guide.md +14 -122
  19. package/docs/v2/guide/03-policy-guide.en.md +6 -50
  20. package/docs/v2/guide/03-policy-guide.md +6 -50
  21. package/docs/v2/guide/04-adapter-guide.en.md +7 -60
  22. package/docs/v2/guide/04-adapter-guide.md +7 -60
  23. package/docs/v2/guide/05-worker-contract.en.md +6 -53
  24. package/docs/v2/guide/05-worker-contract.md +7 -52
  25. package/docs/v2/guide/06-evidence-guide.en.md +8 -44
  26. package/docs/v2/guide/06-evidence-guide.md +9 -43
  27. package/docs/v2/guide/07-approval-guide.en.md +8 -110
  28. package/docs/v2/guide/07-approval-guide.md +8 -121
  29. package/docs/v2/guide/09-security-boundaries.en.md +13 -37
  30. package/docs/v2/guide/09-security-boundaries.md +13 -37
  31. package/docs/v2/guide/10-recovery.en.md +12 -96
  32. package/docs/v2/guide/10-recovery.md +12 -96
  33. package/docs/v2/guide/11-session-handoff.en.md +10 -75
  34. package/docs/v2/guide/11-session-handoff.md +10 -75
  35. package/docs/v2/guide/12-without-runtime.en.md +46 -0
  36. package/docs/v2/guide/12-without-runtime.md +46 -0
  37. package/docs/v2/guide/README.en.md +16 -36
  38. package/docs/v2/guide/README.md +16 -36
  39. package/example/AGENTS.md +15 -69
  40. package/example/README.md +8 -23
  41. package/example/delivery/envelope/prompts/builder.md +3 -1
  42. package/example/delivery/envelope/prompts/fixer.md +3 -1
  43. package/example/delivery/envelope/prompts/reviewer.md +3 -1
  44. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/run-config.yaml +1 -1
  45. package/example//346/214/207/346/214/245/345/217/260.md +8 -40
  46. package/package.json +2 -1
  47. package/src/v2/adapters/notification-payload.js +40 -0
  48. package/src/v2/adapters/shell.js +3 -1
  49. package/src/v2/cli/run-config-check.js +129 -45
  50. package/src/v2/cli/run.js +853 -254
  51. package/src/v2/engine/reducer.js +2 -0
  52. package/src/v2/engine/risk-preset.js +14 -47
  53. package/src/v2/engine/workflow.js +177 -185
  54. package/src/v2/evidence/collector.js +12 -1
  55. package/src/v2/evidence/image.js +346 -0
  56. package/src/v2/policy/policy.js +219 -286
  57. package/src/v2/presets/software-delivery.yaml +1 -16
  58. package/src/v2/runtime/cache.js +22 -1
  59. package/src/v2/runtime/decisions.js +185 -31
  60. package/src/v2/runtime/findings.js +4 -1
  61. package/src/v2/runtime/notify.js +42 -36
  62. package/src/v2/runtime/orchestrator.js +811 -297
  63. package/src/v2/runtime/overview-repos.js +79 -0
  64. package/src/v2/runtime/overview.js +279 -47
  65. package/src/v2/runtime/release.js +233 -0
  66. package/src/v2/runtime/run-record.js +1 -0
  67. package/src/v2/workspace/workspace-manager.js +8 -0
  68. package/templates/v2/AGENTS.md +20 -73
  69. package/templates/v2/BUILDBEAT.md +2 -13
  70. package/templates/v2/envelope/prompts/builder.md +3 -1
  71. package/templates/v2/envelope/prompts/fixer.md +3 -1
  72. package/templates/v2/envelope/prompts/reviewer.md +3 -1
  73. package/templates/v2/run-config.example.yaml +9 -7
  74. package/templates/v2/work.example.md +16 -0
  75. package/templates/v2//346/214/207/346/214/245/345/217/260.md +8 -40
  76. package/docs/v2/skill/01-principles.md +0 -26
  77. package/docs/v2/skill/02-project-layout.md +0 -31
  78. package/docs/v2/skill/03-collaboration-rules.md +0 -45
  79. package/docs/v2/skill/04-rhythm-and-rituals.md +0 -104
  80. package/docs/v2/skill/05-red-lines.md +0 -13
  81. package/docs/v2/skill/06-bootstrap-and-takeover.md +0 -84
  82. package/docs/v2/skill/07-templates-and-lessons.md +0 -24
  83. package/example/.buildbeat/observe.yaml +0 -31
  84. package/src/v2/observe/observe-config.js +0 -194
  85. package/src/v2/observe/observe-reducer.js +0 -117
  86. package/src/v2/observe/observe.js +0 -427
  87. package/src/v2/presets/observe.yaml +0 -45
  88. package/src/v2/presets/policies/ui-render-gate.yaml +0 -13
  89. package/src/v2/presets/release-readback.yaml +0 -36
  90. package/src/v2/presets/risk/controlled.yaml +0 -39
  91. package/src/v2/presets/risk/fast.yaml +0 -19
  92. package/src/v2/presets/risk/release.yaml +0 -21
  93. package/src/v2/presets/risk/standard.yaml +0 -28
  94. package/templates/ARCHITECTURE.md +0 -50
  95. package/templates/contracts/PROTOCOL.md +0 -28
  96. package/templates/pm/adr/ADR-0000-template.md +0 -25
  97. package/templates/pm/adr/README.md +0 -15
  98. package/templates/pm/decisions.md +0 -11
  99. package/templates/standards/CODE.md +0 -23
  100. package/templates/standards/DESIGN.md +0 -36
  101. package/templates/standards/REVIEW.md +0 -20
  102. package/templates/standards/STACK.md +0 -31
@@ -28,9 +28,19 @@ const TOP_KEYS = [
28
28
  "supersede",
29
29
  "stallAfterMs",
30
30
  "parallel",
31
+ "maxReviewSeverity",
32
+ "requireScreenshot",
33
+ "release",
34
+ ];
35
+ const REQUIRED = ["repo", "work", "run"];
36
+ const NULL_REPORTED = [
37
+ "workflow",
38
+ "base",
39
+ "entry",
40
+ "riskPreset",
41
+ "stepTimeoutMs",
42
+ "maxAttemptsPerStep",
31
43
  ];
32
- const REQUIRED = ["repo", "work", "run", "workflow"];
33
- const NULL_REPORTED = ["base", "entry", "riskPreset", "stepTimeoutMs", "maxAttemptsPerStep"];
34
44
  const WORKER_KEYS = ["command", "args", "timeoutMs", "inheritEnv", "env"];
35
45
  const ENVELOPE_KEYS = ["prompts", "vars", "pin"];
36
46
  const LISTS = ["stopAt", "allowedPaths", "policies", "redact", "requires"];
@@ -38,7 +48,9 @@ const ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
38
48
 
39
49
  export class RunConfigError extends Error {
40
50
  constructor(label, problems) {
41
- super(`run config ${label} has ${problems.length} problem(s):\n${problems.map((problem) => ` - ${problem}`).join("\n")}`);
51
+ super(
52
+ `run config ${label} has ${problems.length} problem(s):\n${problems.map((problem) => ` - ${problem}`).join("\n")}`,
53
+ );
42
54
  this.name = "RunConfigError";
43
55
  this.problems = problems;
44
56
  }
@@ -55,7 +67,11 @@ function distance(a, b) {
55
67
  row[0] = i;
56
68
  for (let j = 1; j <= b.length; j += 1) {
57
69
  const kept = row[j];
58
- row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
70
+ row[j] = Math.min(
71
+ row[j] + 1,
72
+ row[j - 1] + 1,
73
+ previous + (a[i - 1] === b[j - 1] ? 0 : 1),
74
+ );
59
75
  previous = kept;
60
76
  }
61
77
  }
@@ -99,7 +115,9 @@ function nonEmptyString(value) {
99
115
  // resolution.
100
116
  export function checkRunConfigShape(config) {
101
117
  if (!isMap(config)) {
102
- return ["the file must be a map of keys (repo, work, run, workflow, workers, ...)"];
118
+ return [
119
+ "the file must be a map of keys (repo, work, run, workflow, workers, ...)",
120
+ ];
103
121
  }
104
122
  const problems = [];
105
123
  for (const key of Object.keys(config)) {
@@ -117,13 +135,17 @@ export function checkRunConfigShape(config) {
117
135
  // (cache: null has always meant no cache; lists must be lists).
118
136
  for (const key of NULL_REPORTED) {
119
137
  if (Object.hasOwn(config, key) && config[key] === null) {
120
- problems.push(`${key}: has no value; remove the line to use the default, or give it a value`);
138
+ problems.push(
139
+ `${key}: has no value; remove the line to use the default, or give it a value`,
140
+ );
121
141
  }
122
142
  }
123
143
  for (const key of ["repo", "workflow", "riskPreset", "base", "entry"]) {
124
144
  const value = config[key];
125
145
  if (value !== undefined && value !== null && !nonEmptyString(value)) {
126
- problems.push(`${key}: must be a non-empty string, got ${JSON.stringify(value)}`);
146
+ problems.push(
147
+ `${key}: must be a non-empty string, got ${JSON.stringify(value)}`,
148
+ );
127
149
  }
128
150
  }
129
151
  for (const key of ["work", "run"]) {
@@ -132,28 +154,51 @@ export function checkRunConfigShape(config) {
132
154
  continue;
133
155
  }
134
156
  if (typeof value !== "string") {
135
- problems.push(`${key}: must be a string, got ${JSON.stringify(value)} (quote it, e.g. ${key}: "${value}")`);
157
+ problems.push(
158
+ `${key}: must be a string, got ${JSON.stringify(value)} (quote it, e.g. ${key}: "${value}")`,
159
+ );
136
160
  } else if (!ID.test(value) || value.includes("..") || value.length > 100) {
137
- problems.push(`${key}: "${value}" may only use letters, digits, ".", "_" and "-", start with a letter or digit, contain no "..", and be at most 100 characters (it becomes part of paths and branch names)`);
161
+ problems.push(
162
+ `${key}: "${value}" may only use letters, digits, ".", "_" and "-", start with a letter or digit, contain no "..", and be at most 100 characters (it becomes part of paths and branch names)`,
163
+ );
138
164
  }
139
165
  }
166
+ if (
167
+ config.maxReviewSeverity !== undefined &&
168
+ !["P2", "P3"].includes(config.maxReviewSeverity)
169
+ ) {
170
+ problems.push("maxReviewSeverity: must be P2 or P3");
171
+ }
172
+ if (Array.isArray(config.policies) && config.policies.length) {
173
+ problems.push(
174
+ "policies: custom policy files are retired; migrate safeguards explicitly (docs/MIGRATION.md)",
175
+ );
176
+ }
140
177
  if (config.parallel !== undefined && typeof config.parallel !== "boolean") {
141
- problems.push(`parallel: must be true or false, got ${JSON.stringify(config.parallel)}`);
178
+ problems.push(
179
+ `parallel: must be true or false, got ${JSON.stringify(config.parallel)}`,
180
+ );
142
181
  }
143
182
  for (const key of ["stepTimeoutMs", "maxAttemptsPerStep"]) {
144
183
  if (config[key] !== undefined && !positiveInteger(config[key])) {
145
- problems.push(`${key}: must be a positive integer, got ${JSON.stringify(config[key])}`);
184
+ problems.push(
185
+ `${key}: must be a positive integer, got ${JSON.stringify(config[key])}`,
186
+ );
146
187
  }
147
188
  }
148
189
  for (const key of LISTS) {
149
190
  if (config[key] !== undefined && !Array.isArray(config[key])) {
150
- problems.push(`${key}: must be a list (one "- item" per line), got ${JSON.stringify(config[key])}`);
191
+ problems.push(
192
+ `${key}: must be a list (one "- item" per line), got ${JSON.stringify(config[key])}`,
193
+ );
151
194
  }
152
195
  }
153
196
  if (Array.isArray(config.allowedPaths)) {
154
197
  config.allowedPaths.forEach((entry, index) => {
155
198
  if (!nonEmptyString(entry)) {
156
- problems.push(`allowedPaths[${index}]: must be a non-empty path, got ${JSON.stringify(entry)}`);
199
+ problems.push(
200
+ `allowedPaths[${index}]: must be a non-empty path, got ${JSON.stringify(entry)}`,
201
+ );
157
202
  }
158
203
  });
159
204
  }
@@ -169,38 +214,65 @@ export function checkRunConfigShape(config) {
169
214
  }
170
215
  }
171
216
  if (config.workers === undefined || config.workers === null) {
172
- problems.push("workers: required and missing (at least the verifier; see templates/v2/run-config.example.yaml)");
217
+ problems.push(
218
+ "workers: required and missing (at least the verifier; see templates/v2/run-config.example.yaml)",
219
+ );
173
220
  } else if (!isMap(config.workers)) {
174
- problems.push("workers: must be a map of worker name -> { command, args, ... }");
221
+ problems.push(
222
+ "workers: must be a map of worker name -> { command, args, ... }",
223
+ );
175
224
  } else {
176
225
  for (const [name, spec] of Object.entries(config.workers)) {
177
- const where = `workers.${name}`;
178
- if (!isMap(spec)) {
179
- problems.push(`${where}: must be a map with at least a command`);
180
- continue;
181
- }
182
- for (const key of Object.keys(spec)) {
183
- if (!WORKER_KEYS.includes(key)) {
184
- problems.push(unknownKey(`${where}.`, key, WORKER_KEYS));
185
- }
186
- }
187
- if (!nonEmptyString(spec.command)) {
188
- problems.push(`${where}.command: required, a non-empty string`);
189
- }
190
- if (spec.args !== undefined && !Array.isArray(spec.args)) {
191
- problems.push(`${where}.args: must be a list (one "- arg" per line)`);
192
- }
193
- if (spec.timeoutMs !== undefined && !(typeof spec.timeoutMs === "number" && spec.timeoutMs > 0)) {
194
- problems.push(`${where}.timeoutMs: must be a positive number, got ${JSON.stringify(spec.timeoutMs)}`);
195
- }
196
- if (spec.inheritEnv !== undefined && typeof spec.inheritEnv !== "boolean") {
197
- problems.push(`${where}.inheritEnv: must be true or false, got ${JSON.stringify(spec.inheritEnv)} (anything else used to mean false silently)`);
198
- }
199
- if (spec.env !== undefined && !isMap(spec.env)) {
200
- problems.push(`${where}.env: must be a map of NAME -> value`);
201
- }
226
+ problems.push(...checkCommandSpec(`workers.${name}`, spec));
202
227
  }
203
228
  }
229
+ if (
230
+ config.requireScreenshot !== undefined &&
231
+ typeof config.requireScreenshot !== "boolean"
232
+ ) {
233
+ problems.push(
234
+ `requireScreenshot: must be true or false, got ${JSON.stringify(config.requireScreenshot)}`,
235
+ );
236
+ }
237
+ // The release readback command has the shape of a worker.
238
+ if (config.release !== undefined) {
239
+ problems.push(...checkCommandSpec("release", config.release));
240
+ }
241
+ return problems;
242
+ }
243
+
244
+ function checkCommandSpec(where, spec) {
245
+ const problems = [];
246
+ if (!isMap(spec)) {
247
+ return [`${where}: must be a map with at least a command`];
248
+ }
249
+ for (const key of Object.keys(spec)) {
250
+ if (!WORKER_KEYS.includes(key)) {
251
+ problems.push(unknownKey(`${where}.`, key, WORKER_KEYS));
252
+ }
253
+ }
254
+ if (!nonEmptyString(spec.command)) {
255
+ problems.push(`${where}.command: required, a non-empty string`);
256
+ }
257
+ if (spec.args !== undefined && !Array.isArray(spec.args)) {
258
+ problems.push(`${where}.args: must be a list (one "- arg" per line)`);
259
+ }
260
+ if (
261
+ spec.timeoutMs !== undefined &&
262
+ !(typeof spec.timeoutMs === "number" && spec.timeoutMs > 0)
263
+ ) {
264
+ problems.push(
265
+ `${where}.timeoutMs: must be a positive number, got ${JSON.stringify(spec.timeoutMs)}`,
266
+ );
267
+ }
268
+ if (spec.inheritEnv !== undefined && typeof spec.inheritEnv !== "boolean") {
269
+ problems.push(
270
+ `${where}.inheritEnv: must be true or false, got ${JSON.stringify(spec.inheritEnv)} (anything else used to mean false silently)`,
271
+ );
272
+ }
273
+ if (spec.env !== undefined && !isMap(spec.env)) {
274
+ problems.push(`${where}.env: must be a map of NAME -> value`);
275
+ }
204
276
  return problems;
205
277
  }
206
278
 
@@ -208,22 +280,34 @@ export function checkRunConfigShape(config) {
208
280
  export function checkRunConfigAgainstWorkflow(config, workflow) {
209
281
  const problems = [];
210
282
  const steps = [...workflow.stepIds];
211
- const workers = [...new Set(workflow.steps.map((step) => step.worker).filter(Boolean))];
283
+ const workers = [
284
+ ...new Set(workflow.steps.map((step) => step.worker).filter(Boolean)),
285
+ ];
212
286
  for (const name of Object.keys(isMap(config.workers) ? config.workers : {})) {
213
287
  if (!workers.includes(name)) {
214
288
  const guess = suggest(name, workers);
215
- problems.push(`workers.${name}: no step of the workflow uses this worker${guess ? ` (did you mean ${guess}?)` : ""}; workers in this workflow: ${workers.join(", ")}`);
289
+ problems.push(
290
+ `workers.${name}: no step of the workflow uses this worker${guess ? ` (did you mean ${guess}?)` : ""}; workers in this workflow: ${workers.join(", ")}`,
291
+ );
216
292
  }
217
293
  }
218
294
  for (const step of Array.isArray(config.stopAt) ? config.stopAt : []) {
219
295
  if (!workflow.stepIds.has(step)) {
220
296
  const guess = suggest(step, steps);
221
- problems.push(`stopAt: "${step}" is not a step of the workflow${guess ? ` (did you mean ${guess}?)` : ""}; steps: ${steps.join(", ")}`);
297
+ problems.push(
298
+ `stopAt: "${step}" is not a step of the workflow${guess ? ` (did you mean ${guess}?)` : ""}; steps: ${steps.join(", ")}`,
299
+ );
222
300
  }
223
301
  }
224
- if (config.entry !== undefined && config.entry !== null && !workflow.stepIds.has(config.entry)) {
302
+ if (
303
+ config.entry !== undefined &&
304
+ config.entry !== null &&
305
+ !workflow.stepIds.has(config.entry)
306
+ ) {
225
307
  const guess = suggest(config.entry, steps);
226
- problems.push(`entry: "${config.entry}" is not a step of the workflow${guess ? ` (did you mean ${guess}?)` : ""}; steps: ${steps.join(", ")}`);
308
+ problems.push(
309
+ `entry: "${config.entry}" is not a step of the workflow${guess ? ` (did you mean ${guess}?)` : ""}; steps: ${steps.join(", ")}`,
310
+ );
227
311
  }
228
312
  return problems;
229
313
  }