@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,302 @@
1
+ // Policy engine per docs/v2/RFC-0003-workflow-policy.md §4. Policies are
2
+ // declarative YAML documents; rules evaluate under three-valued logic
3
+ // (true / false / "unverified") and UNVERIFIED is never treated as PASS.
4
+ // Parsing is fail-closed: unknown operators, types, or fields are rejected.
5
+
6
+ import { createHash } from "node:crypto";
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ export class PolicyError extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "PolicyError";
14
+ }
15
+ }
16
+
17
+ const POLICY_TYPES = ["pre", "post", "transition", "action"];
18
+ const ENFORCEMENTS = ["ADVISORY", "LOCAL_ENFORCED", "SERVER_ENFORCED"];
19
+ const ON_FAIL = ["WAIT_HUMAN", "BLOCK"];
20
+ const POLICY_FIELDS = new Set([
21
+ "kind",
22
+ "version",
23
+ "name",
24
+ "type",
25
+ "appliesTo",
26
+ "enforcement",
27
+ "onFail",
28
+ "rule",
29
+ ]);
30
+ const GRADE_RANK = { L0: 0, L1: 1, L2: 2, L3: 3, L4: 4 };
31
+ const SEVERITY_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
32
+
33
+ const OPERATORS = new Set([
34
+ "all",
35
+ "any",
36
+ "not",
37
+ "evidence.exists",
38
+ "artifact.accepted",
39
+ "attempts.lt",
40
+ "budget.remaining",
41
+ "candidate.clean",
42
+ "human.approved",
43
+ "finding.maxSeverity",
44
+ ]);
45
+
46
+ function validateRule(rule, path = "rule") {
47
+ if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
48
+ throw new PolicyError(`${path} must be a map with exactly one operator`);
49
+ }
50
+ const keys = Object.keys(rule);
51
+ if (keys.length !== 1) {
52
+ throw new PolicyError(`${path} must have exactly one operator, got: ${keys.join(", ")}`);
53
+ }
54
+ const op = keys[0];
55
+ if (!OPERATORS.has(op)) {
56
+ throw new PolicyError(`${path}: unknown operator "${op}"`);
57
+ }
58
+ const arg = rule[op];
59
+ if (op === "all" || op === "any") {
60
+ if (!Array.isArray(arg) || arg.length === 0) {
61
+ throw new PolicyError(`${path}.${op} requires a non-empty list of rules`);
62
+ }
63
+ arg.forEach((child, index) => validateRule(child, `${path}.${op}[${index}]`));
64
+ } else if (op === "not") {
65
+ validateRule(arg, `${path}.not`);
66
+ }
67
+ return rule;
68
+ }
69
+
70
+ export function parsePolicyDoc(doc) {
71
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
72
+ throw new PolicyError("policy document must be a map");
73
+ }
74
+ for (const key of Object.keys(doc)) {
75
+ if (!POLICY_FIELDS.has(key)) {
76
+ throw new PolicyError(`unknown field "${key}" in policy ${doc.name ?? "(unnamed)"}`);
77
+ }
78
+ }
79
+ if (doc.kind !== "policy") {
80
+ throw new PolicyError(`kind must be "policy", got: ${JSON.stringify(doc.kind)}`);
81
+ }
82
+ if (doc.version !== 1) {
83
+ throw new PolicyError(`unsupported policy version: ${JSON.stringify(doc.version)}`);
84
+ }
85
+ if (typeof doc.name !== "string" || doc.name.length === 0) {
86
+ throw new PolicyError("policy needs a name");
87
+ }
88
+ if (!POLICY_TYPES.includes(doc.type)) {
89
+ throw new PolicyError(`policy ${doc.name}: type must be one of ${POLICY_TYPES.join("|")}`);
90
+ }
91
+ if (typeof doc.appliesTo !== "string" || doc.appliesTo.length === 0) {
92
+ throw new PolicyError(`policy ${doc.name}: appliesTo is required`);
93
+ }
94
+ if (!ENFORCEMENTS.includes(doc.enforcement)) {
95
+ throw new PolicyError(
96
+ `policy ${doc.name}: enforcement must be one of ${ENFORCEMENTS.join("|")}`,
97
+ );
98
+ }
99
+ if (doc.onFail !== undefined && !ON_FAIL.includes(doc.onFail)) {
100
+ throw new PolicyError(`policy ${doc.name}: onFail must be one of ${ON_FAIL.join("|")}`);
101
+ }
102
+ validateRule(doc.rule, `policy ${doc.name} rule`);
103
+ return {
104
+ name: doc.name,
105
+ type: doc.type,
106
+ appliesTo: doc.appliesTo,
107
+ enforcement: doc.enforcement,
108
+ onFail: doc.onFail ?? "WAIT_HUMAN",
109
+ rule: doc.rule,
110
+ };
111
+ }
112
+
113
+ function combine(values, mode) {
114
+ // Three-valued logic: false dominates all(), true dominates any(),
115
+ // otherwise any "unverified" makes the result unverified.
116
+ if (mode === "all") {
117
+ if (values.some((value) => value.ok === false)) {
118
+ return values.find((value) => value.ok === false);
119
+ }
120
+ if (values.some((value) => value.ok === "unverified")) {
121
+ return values.find((value) => value.ok === "unverified");
122
+ }
123
+ return { ok: true, why: "all conditions hold" };
124
+ }
125
+ if (values.some((value) => value.ok === true)) {
126
+ return { ok: true, why: "a condition holds" };
127
+ }
128
+ if (values.some((value) => value.ok === "unverified")) {
129
+ return values.find((value) => value.ok === "unverified");
130
+ }
131
+ return { ok: false, why: values.map((value) => value.why).join("; ") };
132
+ }
133
+
134
+ export function sha256Text(text) {
135
+ return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
136
+ }
137
+
138
+ // ctx: { state, workDir, worktreePath, readWorktree() -> {head, dirty} | null }
139
+ export function evaluateRule(rule, ctx) {
140
+ const op = Object.keys(rule)[0];
141
+ const arg = rule[op];
142
+ switch (op) {
143
+ case "all":
144
+ return combine(arg.map((child) => evaluateRule(child, ctx)), "all");
145
+ case "any":
146
+ return combine(arg.map((child) => evaluateRule(child, ctx)), "any");
147
+ case "not": {
148
+ const inner = evaluateRule(arg, ctx);
149
+ if (inner.ok === "unverified") {
150
+ return inner;
151
+ }
152
+ return { ok: !inner.ok, why: `not(${inner.why})` };
153
+ }
154
+ case "evidence.exists": {
155
+ const minRank = arg.minGrade !== undefined ? GRADE_RANK[arg.minGrade] : 0;
156
+ if (minRank === undefined) {
157
+ return { ok: "unverified", why: `unknown grade ${arg.minGrade}` };
158
+ }
159
+ // When the context names a candidate (e.g. a merge approval), only
160
+ // evidence about that candidate counts — facts about superseded
161
+ // candidates must not satisfy a gate for the current one.
162
+ const scoped = ctx.candidate
163
+ ? ctx.state.evidence.filter((item) => item.subject === ctx.candidate)
164
+ : ctx.state.evidence;
165
+ const hit = scoped.some(
166
+ (item) =>
167
+ item.kind === arg.kind &&
168
+ item.status === "passed" &&
169
+ (GRADE_RANK[item.grade] ?? -1) >= minRank,
170
+ );
171
+ return hit
172
+ ? { ok: true, why: `evidence ${arg.kind} present` }
173
+ : {
174
+ ok: false,
175
+ why: `no passed evidence of kind ${arg.kind}${arg.minGrade ? ` at grade >= ${arg.minGrade}` : ""}${ctx.candidate ? " for the current candidate" : ""}`,
176
+ };
177
+ }
178
+ case "artifact.accepted": {
179
+ const artifact = arg.artifact;
180
+ const filePath = join(ctx.workDir, `${artifact}.md`);
181
+ if (!existsSync(filePath)) {
182
+ return { ok: "unverified", why: `artifact file missing: ${artifact}.md` };
183
+ }
184
+ const decisionsPath = join(ctx.workDir, "decisions.jsonl");
185
+ if (!existsSync(decisionsPath)) {
186
+ return { ok: false, why: `artifact ${artifact} has never been accepted` };
187
+ }
188
+ const accepts = readFileSync(decisionsPath, "utf8")
189
+ .split("\n")
190
+ .filter(Boolean)
191
+ .map((line) => JSON.parse(line))
192
+ .filter(
193
+ (line) => line.transition === `accept-${artifact}` && line.decision === "approved",
194
+ );
195
+ if (accepts.length === 0) {
196
+ return { ok: false, why: `artifact ${artifact} has never been accepted` };
197
+ }
198
+ const latest = accepts[accepts.length - 1];
199
+ const currentDigest = sha256Text(readFileSync(filePath, "utf8"));
200
+ if (latest.subject?.digest !== currentDigest) {
201
+ return {
202
+ ok: false,
203
+ why: `artifact ${artifact} changed after its acceptance (stale acceptance)`,
204
+ };
205
+ }
206
+ return { ok: true, why: `artifact ${artifact} accepted at current digest` };
207
+ }
208
+ case "attempts.lt": {
209
+ const attempts = ctx.state.steps[arg.step]?.attempts ?? 0;
210
+ return attempts < arg.max
211
+ ? { ok: true, why: `${arg.step} attempts ${attempts} < ${arg.max}` }
212
+ : { ok: false, why: `${arg.step} attempts ${attempts} reached ${arg.max}` };
213
+ }
214
+ case "budget.remaining": {
215
+ const budget = ctx.state.budgets[arg.kind];
216
+ if (!budget) {
217
+ return { ok: true, why: `budget ${arg.kind} untouched` };
218
+ }
219
+ return budget.remaining > 0
220
+ ? { ok: true, why: `budget ${arg.kind} remaining ${budget.remaining}` }
221
+ : { ok: false, why: `budget ${arg.kind} exhausted` };
222
+ }
223
+ case "candidate.clean": {
224
+ const tree = ctx.readWorktree();
225
+ if (!tree) {
226
+ return { ok: "unverified", why: "worktree not available for readback" };
227
+ }
228
+ return tree.dirty
229
+ ? { ok: false, why: "worktree is dirty" }
230
+ : { ok: true, why: "worktree is clean" };
231
+ }
232
+ case "human.approved": {
233
+ const hit = ctx.state.approvals.some(
234
+ (approval) => !approval.stale && approval.transition === arg.transition,
235
+ );
236
+ return hit
237
+ ? { ok: true, why: `transition ${arg.transition} approved` }
238
+ : { ok: false, why: `no active approval for transition ${arg.transition}` };
239
+ }
240
+ case "finding.maxSeverity": {
241
+ const atMostRank = SEVERITY_RANK[arg.atMost];
242
+ if (atMostRank === undefined) {
243
+ return { ok: "unverified", why: `unknown severity ${arg.atMost}` };
244
+ }
245
+ // Findings against superseded candidates are history, not verdicts:
246
+ // a blocked round-1 review that the fix loop already addressed must
247
+ // not poison the gate for the final candidate. When the context
248
+ // names a candidate, judge only reviews of that candidate.
249
+ const reviews = ctx.state.evidence.filter(
250
+ (item) =>
251
+ item.kind === "review" && (!ctx.candidate || item.subject === ctx.candidate),
252
+ );
253
+ if (reviews.length === 0) {
254
+ return {
255
+ ok: "unverified",
256
+ why: ctx.candidate
257
+ ? "no review evidence for the current candidate"
258
+ : "no review evidence to judge findings",
259
+ };
260
+ }
261
+ const findings = reviews.flatMap((item) => item.findings ?? []);
262
+ const tooSevere = findings.filter(
263
+ (finding) => (SEVERITY_RANK[finding.severity] ?? 0) < atMostRank,
264
+ );
265
+ return tooSevere.length === 0
266
+ ? { ok: true, why: `no findings above ${arg.atMost}` }
267
+ : {
268
+ ok: false,
269
+ why: `${tooSevere.length} finding(s) more severe than ${arg.atMost}`,
270
+ };
271
+ }
272
+ default:
273
+ return { ok: "unverified", why: `unknown operator ${op}` };
274
+ }
275
+ }
276
+
277
+ // Evaluates every policy of `type` whose appliesTo matches; returns rows of
278
+ // { policy, result, reason, enforcement } where result is a GateResult.
279
+ export function evaluatePolicies(policies, { type, appliesTo }, ctx) {
280
+ const rows = [];
281
+ for (const policy of policies ?? []) {
282
+ if (policy.type !== type || policy.appliesTo !== appliesTo) {
283
+ continue;
284
+ }
285
+ const verdict = evaluateRule(policy.rule, ctx);
286
+ let result;
287
+ if (verdict.ok === true) {
288
+ result = "PASS";
289
+ } else if (verdict.ok === "unverified") {
290
+ result = "UNVERIFIED";
291
+ } else {
292
+ result = policy.onFail;
293
+ }
294
+ rows.push({
295
+ policy: policy.name,
296
+ result,
297
+ reason: verdict.why,
298
+ enforcement: policy.enforcement,
299
+ });
300
+ }
301
+ return rows;
302
+ }
@@ -0,0 +1,45 @@
1
+ # observe workflow preset per docs/v2/RFC-0003-workflow-policy.md §8 (frozen
2
+ # shape; block form — the strict YAML subset has no inline {}/[]).
3
+ # One `observe run` invocation = one cycle; scheduling is host-level in v0
4
+ # (cron/CI invoking the CLI). Copy to <repo>/.buildbeat/observe.yaml and
5
+ # point commands at the project's real probes.
6
+ kind: workflow
7
+ version: 1
8
+ name: observe
9
+ providers:
10
+ - id: drift-check
11
+ command: bash
12
+ args:
13
+ - scripts/drift-check.sh
14
+ schedule: manual
15
+ evidence:
16
+ kind: drift
17
+ subject: deploy-unit
18
+ severity:
19
+ failed: error
20
+ unverified: warn
21
+ diagnose:
22
+ command: bash
23
+ args:
24
+ - scripts/live-status.sh
25
+ - id: live-status
26
+ command: bash
27
+ args:
28
+ - scripts/live-status.sh
29
+ schedule: manual
30
+ evidence:
31
+ kind: runtime-health
32
+ subject: deploy-unit
33
+ bands:
34
+ - level: log
35
+ when: "severity >= info"
36
+ - level: diagnose
37
+ when: "severity >= error"
38
+ - level: intent
39
+ when: "severity >= error"
40
+ triage:
41
+ actions:
42
+ - fix_now
43
+ - schedule
44
+ - dismiss
45
+ dismissFeedback: bands
@@ -0,0 +1,13 @@
1
+ # Invariant 22 (v1 lessons #3, machine form): a UI delivery's spec approval
2
+ # subject must include renderable proof — a screenshot with a digest —
3
+ # before a human may stamp it. Static prose is not an approvable design.
4
+ kind: policy
5
+ version: 1
6
+ name: ui-render-gate
7
+ type: transition
8
+ appliesTo: enter-spec
9
+ enforcement: LOCAL_ENFORCED
10
+ rule:
11
+ evidence.exists:
12
+ kind: screenshot
13
+ minGrade: L2
@@ -0,0 +1,39 @@
1
+ # Controlled lane: intent AND plan must be accepted artifacts before build;
2
+ # merge stays human behind a stricter evidence floor. The release gate from
3
+ # the controlled track is out of MVP scope (no deploy capability exists) and
4
+ # returns with observe/release work in M5.
5
+ kind: risk-preset
6
+ version: 1
7
+ name: controlled
8
+ policies:
9
+ - kind: policy
10
+ version: 1
11
+ name: intent-accepted
12
+ type: pre
13
+ appliesTo: build
14
+ enforcement: LOCAL_ENFORCED
15
+ rule:
16
+ artifact.accepted:
17
+ artifact: intent
18
+ - kind: policy
19
+ version: 1
20
+ name: plan-accepted
21
+ type: pre
22
+ appliesTo: build
23
+ enforcement: LOCAL_ENFORCED
24
+ rule:
25
+ artifact.accepted:
26
+ artifact: plan
27
+ - kind: policy
28
+ version: 1
29
+ name: merge-evidence-floor
30
+ type: transition
31
+ appliesTo: enter-wait-merge
32
+ enforcement: LOCAL_ENFORCED
33
+ rule:
34
+ all:
35
+ - evidence.exists:
36
+ kind: command
37
+ minGrade: L2
38
+ - finding.maxSeverity:
39
+ atMost: P3
@@ -0,0 +1,19 @@
1
+ # Fast lane: automatic plan/build/verify/review, human merge decision only.
2
+ # The merge gate still demands read-back evidence and a clean review.
3
+ kind: risk-preset
4
+ version: 1
5
+ name: fast
6
+ policies:
7
+ - kind: policy
8
+ version: 1
9
+ name: merge-evidence-floor
10
+ type: transition
11
+ appliesTo: enter-wait-merge
12
+ enforcement: LOCAL_ENFORCED
13
+ rule:
14
+ all:
15
+ - evidence.exists:
16
+ kind: command
17
+ minGrade: L2
18
+ - finding.maxSeverity:
19
+ atMost: P2
@@ -0,0 +1,44 @@
1
+ # Migration preset mapping BuildBeat v1's four fixed gates:
2
+ # Gate1 (requirements) -> intent acceptance (digest-bound)
3
+ # Gate2 (design) -> plan acceptance + human boundary before build
4
+ # Gate3 (merge) -> the built-in final merge decision
5
+ # Gate4 (release) -> out of MVP scope (no deploy capability exists)
6
+ # The extra human boundary before review preserves v1's late review gate.
7
+ kind: risk-preset
8
+ version: 1
9
+ name: legacy-four-gates
10
+ stopAt:
11
+ - build
12
+ - review
13
+ policies:
14
+ - kind: policy
15
+ version: 1
16
+ name: intent-accepted
17
+ type: pre
18
+ appliesTo: build
19
+ enforcement: LOCAL_ENFORCED
20
+ rule:
21
+ artifact.accepted:
22
+ artifact: intent
23
+ - kind: policy
24
+ version: 1
25
+ name: plan-accepted
26
+ type: pre
27
+ appliesTo: build
28
+ enforcement: LOCAL_ENFORCED
29
+ rule:
30
+ artifact.accepted:
31
+ artifact: plan
32
+ - kind: policy
33
+ version: 1
34
+ name: merge-evidence-floor
35
+ type: transition
36
+ appliesTo: enter-wait-merge
37
+ enforcement: LOCAL_ENFORCED
38
+ rule:
39
+ all:
40
+ - evidence.exists:
41
+ kind: command
42
+ minGrade: L2
43
+ - finding.maxSeverity:
44
+ atMost: P2
@@ -0,0 +1,28 @@
1
+ # Standard lane: the plan must be an accepted artifact (digest-bound) before
2
+ # build starts; merge stays a human decision behind the evidence floor.
3
+ kind: risk-preset
4
+ version: 1
5
+ name: standard
6
+ policies:
7
+ - kind: policy
8
+ version: 1
9
+ name: plan-accepted
10
+ type: pre
11
+ appliesTo: build
12
+ enforcement: LOCAL_ENFORCED
13
+ rule:
14
+ artifact.accepted:
15
+ artifact: plan
16
+ - kind: policy
17
+ version: 1
18
+ name: merge-evidence-floor
19
+ type: transition
20
+ appliesTo: enter-wait-merge
21
+ enforcement: LOCAL_ENFORCED
22
+ rule:
23
+ all:
24
+ - evidence.exists:
25
+ kind: command
26
+ minGrade: L2
27
+ - finding.maxSeverity:
28
+ atMost: P2
@@ -0,0 +1,39 @@
1
+ # Official software-delivery preset per docs/v2/RFC-0003-workflow-policy.md §2.
2
+ # Step order defines the happy path (each step chains to the next on
3
+ # "succeeded"); fix sits off-path at the end and is reached only via the
4
+ # explicit failure/findings transitions.
5
+ kind: workflow
6
+ version: 1
7
+ name: software-delivery
8
+ entry: intent
9
+ steps:
10
+ - id: intent
11
+ worker: planner
12
+ - id: spec
13
+ worker: planner
14
+ optional: true
15
+ requiredWhen: ui-delivery
16
+ - id: plan
17
+ worker: planner
18
+ - id: build
19
+ worker: builder
20
+ - id: verify
21
+ worker: verifier
22
+ - id: review
23
+ worker: reviewer
24
+ readonly: true
25
+ - id: wait-merge
26
+ - id: fix
27
+ worker: fixer
28
+ transitions:
29
+ - from: verify
30
+ on: failed
31
+ to: fix
32
+ - from: fix
33
+ on: succeeded
34
+ to: verify
35
+ - from: review
36
+ on: findings-blocking
37
+ to: fix
38
+ terminal:
39
+ - wait-merge